From c66b080d77ea768a1b8d4eeebbea7312f30e0fdb Mon Sep 17 00:00:00 2001 From: divyanshu_Kumar <154233802+d1vyanshu-kumar@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:37:04 +0530 Subject: [PATCH 001/129] usage-setting --- .../application/FormUsageContract.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/application/FormUsageContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/application/FormUsageContract.java index 90858e527..33d0b69b5 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/application/FormUsageContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/application/FormUsageContract.java @@ -10,6 +10,8 @@ public class FormUsageContract { private Long formId; private String formElementGroupUUID; private String formElementUUID; + private String formElementGroupName; + private String formElementName; static public FormUsageContract fromEntity(FormElement formElement) { FormUsageContract formUsageContract = new FormUsageContract(); @@ -20,6 +22,8 @@ static public FormUsageContract fromEntity(FormElement formElement) { formUsageContract.setFormId(form.getId()); formUsageContract.setFormName(form.getName()); formUsageContract.setFormUUID(form.getUuid()); + formUsageContract.setFormElementGroupName(formElementGroup.getName()); + formUsageContract.setformElementName(formElement.getName()); return formUsageContract; } @@ -62,4 +66,21 @@ public String getFormElementUUID() { public void setFormElementUUID(String formElementUUID) { this.formElementUUID = formElementUUID; } + + public void setFormElementGroupName(String formElementGroupName){ + this.formElementGroupName = formElementGroupName; + } + public String getFormElementGroupName(){ + return formElementGroupName ; + } + + public void setformElementName(String formElementName){ + this.formElementName = formElementName; + } + + public String getformElementName(){ + return formElementName; + } + + } From e10b649449f080702e8e75c0f497d0d71727a80e Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 28 Jun 2024 15:16:04 +0530 Subject: [PATCH 002/129] avniproject/avni-webapp#763 - move title lineage retrieval from database to in memory. --- .../avni/server/dao/LocationRepository.java | 7 ++- .../dao/search/SubjectSearchQueryBuilder.java | 3 +- .../org/avni/server/domain/AddressLevel.java | 22 ++++---- .../avni/server/domain/AddressLevelType.java | 5 ++ .../server/service/AddressLevelService.java | 54 +++++++++++++------ .../service/IndividualSearchService.java | 10 +++- .../avni/server/domain/AddressLevelTest.java | 14 +++++ .../domain/factory/AddressLevelBuilder.java | 10 ++++ .../factory/AddressLevelTypeBuilder.java | 10 ++++ .../AddressLevelServiceIntegrationTest.java | 48 +++++++++++++++++ .../service/AddressLevelServiceTest.java | 1 - .../service/builder/TestLocationService.java | 4 ++ .../src/test/resources/tear-down.sql | 1 + 13 files changed, 152 insertions(+), 37 deletions(-) create mode 100644 avni-server-api/src/test/java/org/avni/server/domain/AddressLevelTest.java create mode 100644 avni-server-api/src/test/java/org/avni/server/service/AddressLevelServiceIntegrationTest.java diff --git a/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java index ba47fc742..a95e80b26 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java @@ -18,10 +18,7 @@ import javax.persistence.QueryHint; import javax.validation.constraints.NotNull; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; @Repository @RepositoryRestResource(collectionResourceRel = "locations", path = "locations") @@ -263,4 +260,6 @@ default Optional findByTitleLineageIgnoreCase(String locationTitle List findByTitleAndType(String title, AddressLevelType lowestAddressLevelType, Pageable pageable); AddressLevel findByTitleAndTypeAndIsVoidedFalse(String title, AddressLevelType addressLevelType); + + List findAllByIdIn(List addressIds); } diff --git a/avni-server-api/src/main/java/org/avni/server/dao/search/SubjectSearchQueryBuilder.java b/avni-server-api/src/main/java/org/avni/server/dao/search/SubjectSearchQueryBuilder.java index 2e61f9910..c1cc89646 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/search/SubjectSearchQueryBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/search/SubjectSearchQueryBuilder.java @@ -12,12 +12,11 @@ public SqlQuery build() { " i.profile_picture as \"profilePicture\",\n" + " cast(concat_ws(' ',i.first_name,i.middle_name,i.last_name)as text) as \"fullName\",\n" + " i.uuid as \"uuid\",\n" + - " cast(tllv.title_lineage as text) as \"addressLevel\",\n" + + " i.address_id as \"addressId\",\n" + " st.name as \"subjectTypeName\",\n" + " gender.name as \"gender\",\n" + " i.date_of_birth as \"dateOfBirth\" $CUSTOM_FIELDS\n" + "from individual i\n" + - " left outer join title_lineage_locations_view tllv on i.address_id = tllv.lowestpoint_id\n" + " left outer join gender on i.gender_id = gender.id\n" + " left outer join subject_type st on i.subject_type_id = st.id and st.is_voided is false\n"; return super.buildUsingBaseQuery(baseQuery, ""); diff --git a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java index 3d8a32291..e0ee10ed2 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java @@ -11,9 +11,7 @@ import javax.persistence.*; import javax.validation.constraints.NotNull; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; @Entity @@ -46,9 +44,6 @@ public class AddressLevel extends OrganisationAwareEntity { @Type(type = "org.avni.server.ltree.LTreeType") private String lineage; - @Transient - private String typeString; - @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "location") private Set parentLocationMappings = new HashSet<>(); @@ -163,10 +158,6 @@ public String getTypeString() { return this.type.getName(); } - public void setTypeString(String typeString) { - this.typeString = typeString; - } - public void addCatchment(Catchment catchment) { catchments.add(catchment); } @@ -193,7 +184,7 @@ public void setVirtualCatchments(Set virtualCatchments) { public void setParentLocationMapping(ParentLocationMapping parentLocationMapping) { AddressLevel parentLocation = parentLocationMappings.stream() - .map(it -> it.getParentLocation()) + .map(ParentLocationMapping::getParentLocation) .filter(Objects::nonNull) .findFirst().orElse(null); if (!parentLocationMapping.getParentLocation().equals(parentLocation)) { @@ -252,6 +243,11 @@ public void setLegacyId(String legacyId) { this.legacyId = legacyId; } + public void addChild(AddressLevel addressLevel) { + this.subLocations.add(addressLevel); + addressLevel.setParent(this); + } + @Projection(name = "AddressLevelProjection", types = {AddressLevel.class}) public interface AddressLevelProjection extends BaseProjection { String getTitle(); @@ -262,4 +258,8 @@ public interface AddressLevelProjection extends BaseProjection { public void calculateLineage() { this.lineage = this.getParent() == null ? this.getId().toString() : this.getParent().lineage + "." + this.getId().toString(); } + + public List getLineageAddressIds() { + return Arrays.stream(this.lineage.split("\\.")).map(Long::parseLong).collect(Collectors.toList()); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevelType.java b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevelType.java index b8b787889..14b8d5d99 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevelType.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevelType.java @@ -80,6 +80,11 @@ public void setAddressLevels(Set addressLevels) { this.addressLevels = addressLevels; } + public void addChildAddressLevelType(AddressLevelType addressLevelType) { + subTypes.add(addressLevelType); + addressLevelType.setParent(this); + } + @JsonIgnore public Boolean isVoidable() { return subTypes.stream().allMatch(CHSEntity::isVoided) && addressLevels.stream().allMatch(CHSEntity::isVoided); diff --git a/avni-server-api/src/main/java/org/avni/server/service/AddressLevelService.java b/avni-server-api/src/main/java/org/avni/server/service/AddressLevelService.java index 5307bab54..2fe04d147 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/AddressLevelService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/AddressLevelService.java @@ -12,9 +12,7 @@ import org.avni.server.web.request.webapp.SubjectTypeSetting; import org.springframework.stereotype.Service; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -24,7 +22,18 @@ public class AddressLevelService { private final AddressLevelTypeRepository addressLevelTypeRepository; private final OrganisationConfigService organisationConfigService; private final AddressLevelCache addressLevelCache; - private ObjectMapper objectMapper; + private final ObjectMapper objectMapper; + + public AddressLevelService(LocationRepository locationRepository, + AddressLevelTypeRepository addressLevelTypeRepository, + OrganisationConfigService organisationConfigService, + AddressLevelCache addressLevelCache) { + this.locationRepository = locationRepository; + this.addressLevelTypeRepository = addressLevelTypeRepository; + this.organisationConfigService = organisationConfigService; + this.addressLevelCache = addressLevelCache; + this.objectMapper = ObjectMapperSingleton.getObjectMapper(); + } public List getAddressLevelsByCatchmentAndSubjectType(Catchment catchment, SubjectType subjectType) { return filterByCatchmentAndSubjectType(catchment, subjectType) @@ -53,22 +62,10 @@ private Optional getCustomRegistrationSetting(SubjectType su organisationConfigService.getSettingsByKey(KeyType.customRegistrationLocations.toString()), new TypeReference>() { }); - Optional customLocationTypes = customRegistrationLocations.stream() + return customRegistrationLocations.stream() .filter(crl -> crl.getSubjectTypeUUID() .equals(subjectType.getUuid())) .findFirst(); - return customLocationTypes; - } - - public AddressLevelService(LocationRepository locationRepository, - AddressLevelTypeRepository addressLevelTypeRepository, - OrganisationConfigService organisationConfigService, - AddressLevelCache addressLevelCache) { - this.locationRepository = locationRepository; - this.addressLevelTypeRepository = addressLevelTypeRepository; - this.organisationConfigService = organisationConfigService; - this.addressLevelCache = addressLevelCache; - this.objectMapper = ObjectMapperSingleton.getObjectMapper(); } public List getAllLocations() { @@ -100,4 +97,27 @@ public List getAllRegistrationAddressIdsBySubjectType(Catchment catchment, public String getTitleLineage(AddressLevel location) { return locationRepository.getTitleLineageById(location.getId()); } + + // This method uses in memory approach instead of database, because for smaller number of addresses the query plan to achive this is expensive due to over estimation by postgres. + public Map getTitleLineages(List addressIds) { + List addresses = locationRepository.findAllByIdIn(addressIds); + + HashSet uniqueAddresses = new HashSet<>(); + addresses.forEach(addressLevel -> uniqueAddresses.addAll(addressLevel.getLineageAddressIds())); + List allAddressesInScope = locationRepository.findAllByIdIn(new ArrayList<>(uniqueAddresses)); + + HashMap titleLineages = new HashMap<>(); + addressIds.forEach(addressId -> { + final AddressLevel addressLevel = addresses.stream().filter(al -> al.getId().equals(addressId)).findFirst().orElseThrow(() -> new AssertionError("Address not found")); + + String lineage = allAddressesInScope.stream() + .filter(inScopeAddress -> addressLevel.getLineageAddressIds().contains(inScopeAddress.getId())) + .sorted((o1, o2) -> o2.getLevel().compareTo(o1.getLevel())) + .map(AddressLevel::getTitle) + .collect(Collectors.joining(", ")); + + titleLineages.put(addressId, lineage); + }); + return titleLineages; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/IndividualSearchService.java b/avni-server-api/src/main/java/org/avni/server/service/IndividualSearchService.java index 6af81209e..d64a69e0e 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IndividualSearchService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IndividualSearchService.java @@ -1,6 +1,5 @@ package org.avni.server.service; -import org.avni.server.dao.IndividualRepository; import org.avni.server.dao.ProgramEnrolmentRepository; import org.avni.server.dao.SubjectSearchRepository; import org.avni.server.dao.search.SubjectSearchQueryBuilder; @@ -19,11 +18,13 @@ public class IndividualSearchService { private final SubjectSearchRepository subjectSearchRepository; private final ProgramEnrolmentRepository programEnrolmentRepository; + private final AddressLevelService addressLevelService; @Autowired - public IndividualSearchService(SubjectSearchRepository subjectSearchRepository, IndividualRepository individualRepository, ProgramEnrolmentRepository programEnrolmentRepository) { + public IndividualSearchService(SubjectSearchRepository subjectSearchRepository, ProgramEnrolmentRepository programEnrolmentRepository, AddressLevelService addressLevelService) { this.subjectSearchRepository = subjectSearchRepository; this.programEnrolmentRepository = programEnrolmentRepository; + this.addressLevelService = addressLevelService; } public LinkedHashMap search(SubjectSearchRequest subjectSearchRequest) { @@ -37,7 +38,11 @@ private LinkedHashMap constructIndividual(List individualIds = individualList.stream() .map(individualRecord -> Long.valueOf((Integer) individualRecord.get("id"))) .collect(Collectors.toList()); + List addressIds = individualList.stream() + .map(individualRecord -> ((BigInteger) individualRecord.get("addressId")).longValue()) + .collect(Collectors.toList()); List searchSubjectEnrolledPrograms = programEnrolmentRepository.findActiveEnrolmentsByIndividualIds(individualIds); + Map titleLineages = addressLevelService.getTitleLineages(addressIds); List> listOfRecords = individualList.stream() .peek(individualRecord -> { @@ -46,6 +51,7 @@ private LinkedHashMap constructIndividual(List x.getId().equals(individualId)) .map(SearchSubjectEnrolledProgram::getProgram) .collect(Collectors.toList())); + individualRecord.put("addressLevel", titleLineages.get(((BigInteger) individualRecord.get("addressId")).longValue())); }).collect(Collectors.toList()); recordsMap.put("totalElements", totalCount); recordsMap.put("listOfRecords", listOfRecords); diff --git a/avni-server-api/src/test/java/org/avni/server/domain/AddressLevelTest.java b/avni-server-api/src/test/java/org/avni/server/domain/AddressLevelTest.java new file mode 100644 index 000000000..3fbc21bc8 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/domain/AddressLevelTest.java @@ -0,0 +1,14 @@ +package org.avni.server.domain; + +import org.avni.server.domain.factory.AddressLevelBuilder; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class AddressLevelTest { + @Test + public void getAddressIds() { + assertEquals(123, new AddressLevelBuilder().withLineage("123").build().getLineageAddressIds().get(0).longValue()); + assertEquals(456, new AddressLevelBuilder().withLineage("123.456").build().getLineageAddressIds().get(1).longValue()); + } +} diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java index 511c61e9e..530f5042e 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java @@ -27,6 +27,11 @@ public AddressLevelBuilder parent(AddressLevel parent) { return this; } + public AddressLevelBuilder child(AddressLevel addressLevel) { + entity.addChild(addressLevel); + return this; + } + public AddressLevelBuilder id(long id) { entity.setId(id); return this; @@ -37,6 +42,11 @@ public AddressLevelBuilder withUuid(String uuid) { return this; } + public AddressLevelBuilder withLineage(String lineage) { + entity.setLineage(lineage); + return this; + } + public AddressLevelBuilder withDefaultValuesForNewEntity() { String s = UUID.randomUUID().toString(); return withUuid(s).title(s); diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelTypeBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelTypeBuilder.java index b6deab513..5b2465369 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelTypeBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelTypeBuilder.java @@ -27,6 +27,16 @@ public AddressLevelTypeBuilder withDefaultValuesForNewEntity() { return withUuid(placeholder).name(placeholder).level(3d); } + public AddressLevelTypeBuilder parent(AddressLevelType parent) { + addressLevelType.setParent(parent); + return this; + } + + public AddressLevelTypeBuilder child(AddressLevelType child) { + addressLevelType.addChildAddressLevelType(child); + return this; + } + public AddressLevelType build() { return this.addressLevelType; } diff --git a/avni-server-api/src/test/java/org/avni/server/service/AddressLevelServiceIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/service/AddressLevelServiceIntegrationTest.java new file mode 100644 index 000000000..ea565f2fa --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/service/AddressLevelServiceIntegrationTest.java @@ -0,0 +1,48 @@ +package org.avni.server.service; + +import org.avni.server.common.AbstractControllerIntegrationTest; +import org.avni.server.dao.AddressLevelTypeRepository; +import org.avni.server.domain.AddressLevel; +import org.avni.server.domain.AddressLevelType; +import org.avni.server.domain.factory.AddressLevelBuilder; +import org.avni.server.domain.factory.AddressLevelTypeBuilder; +import org.avni.server.service.builder.TestDataSetupService; +import org.avni.server.service.builder.TestLocationService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +import java.util.Arrays; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +public class AddressLevelServiceIntegrationTest extends AbstractControllerIntegrationTest { + @Autowired + private TestDataSetupService testDataSetupService; + @Autowired + private AddressLevelTypeRepository addressLevelTypeRepository; + @Autowired + private TestLocationService testLocationService; + @Autowired + private AddressLevelService addressLevelService; + + @Test + public void getTitleLineages() { + testDataSetupService.setupOrganisation(); + AddressLevelType city = addressLevelTypeRepository.save(new AddressLevelTypeBuilder().withUuid("city").name("city").level(1d).build()); + AddressLevelType state = addressLevelTypeRepository.save(new AddressLevelTypeBuilder().withUuid("state").name("state").level(2d).child(city).build()); + + AddressLevel bangalore = testLocationService.save(new AddressLevelBuilder().withUuid("bangalore").title("bangalore").type(city).build()); + AddressLevel kochi = testLocationService.save(new AddressLevelBuilder().withUuid("kochi").title("kochi").type(city).build()); + + testLocationService.save(new AddressLevelBuilder().withUuid("karnataka").title("karnataka").type(state).child(bangalore).build()); + testLocationService.save(new AddressLevelBuilder().withUuid("kerala").title("kerala").type(state).child(kochi).build()); + + Map titleLineages = addressLevelService.getTitleLineages(Arrays.asList(bangalore.getId(), kochi.getId())); + assertEquals("karnataka, bangalore", titleLineages.get(bangalore.getId())); + assertEquals("kerala, kochi", titleLineages.get(kochi.getId())); + } +} diff --git a/avni-server-api/src/test/java/org/avni/server/service/AddressLevelServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/AddressLevelServiceTest.java index bd7a2881e..0e29da453 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/AddressLevelServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/AddressLevelServiceTest.java @@ -21,7 +21,6 @@ import static org.mockito.Mockito.when; public class AddressLevelServiceTest { - @Test public void shouldFetchDifferentAddressIdsWhenSubjectTypeChanges() throws JsonProcessingException { LocationRepository locationRepository = mock(LocationRepository.class); diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestLocationService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestLocationService.java index 4612425e7..e961845d0 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestLocationService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestLocationService.java @@ -18,6 +18,10 @@ public AddressLevel save(AddressLevel location) { locationRepository.save(location); location.calculateLineage(); locationRepository.save(location); + location.getSubLocations().forEach(addressLevel -> { + addressLevel.calculateLineage(); + locationRepository.save(addressLevel); + }); return location; } } diff --git a/avni-server-api/src/test/resources/tear-down.sql b/avni-server-api/src/test/resources/tear-down.sql index 3f7500b68..8375a5392 100644 --- a/avni-server-api/src/test/resources/tear-down.sql +++ b/avni-server-api/src/test/resources/tear-down.sql @@ -27,6 +27,7 @@ DELETE FROM encounter_type where 1 = 1; DELETE FROM gender where 1 = 1; DELETE FROM catchment_address_mapping where 1 = 1; DELETE FROM address_level where 1 = 1; +DELETE FROM address_level_type where 1 = 1; DELETE FROM catchment where 1 = 1; DELETE FROM account_admin where 1 = 1; DELETE FROM user_group where 1 = 1; From e7d684268315562b90b8abd1aab0deb8a55a984b Mon Sep 17 00:00:00 2001 From: Joy A Date: Fri, 28 Jun 2024 18:15:33 +0530 Subject: [PATCH 003/129] avniproject/avni-webapp#1263 | Add duration input for 'Recent' type standard report cards --- .../org/avni/server/domain/ReportCard.java | 22 +++++++++- .../mapper/dashboard/ReportCardMapper.java | 19 ++++++++ .../org/avni/server/service/CardService.java | 19 ++++++-- .../avni/server/web/contract/ValueUnit.java | 43 +++++++++++++++++++ .../request/reports/ReportCardRequest.java | 10 +++++ .../reports/ReportCardBundleContract.java | 6 +++ .../reports/ReportCardWebResponse.java | 10 +++++ .../V1_342__UpdateStandardCardTypeNames.sql | 9 ++++ 8 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/web/contract/ValueUnit.java create mode 100644 avni-server-api/src/main/resources/db/migration/V1_342__UpdateStandardCardTypeNames.sql diff --git a/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java b/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java index 771b4a95f..f958bbffa 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.avni.server.web.contract.ValueUnit; import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.Type; @@ -143,10 +144,29 @@ private List safeGetterForStandardReportCardInput(String key) { return standardReportCardInput == null ? new ArrayList() : standardReportCardInput.getList(key); } + private String getterForStandardReportCardInputString(String key) { + return standardReportCardInput.getString(key); + } + private void safeSetterForStandardReportCardInput(String key, List value) { - if(standardReportCardInput == null) { + if (standardReportCardInput == null) { standardReportCardInput = new JsonObject(new HashMap<>()); } standardReportCardInput.with(key, value); } + + private void safeSetterForStandardReportCardInput(String key, String value) { + if (standardReportCardInput == null) { + standardReportCardInput = new JsonObject(new HashMap<>()); + } + standardReportCardInput.with(key, value); + } + + public String getStandardReportCardInputRecentDuration() { + return getterForStandardReportCardInputString("recentDuration"); + } + + public void setStandardReportCardInputRecentDuration(ValueUnit standardReportCardInputRecentDuration) { + safeSetterForStandardReportCardInput("recentDuration", standardReportCardInputRecentDuration.toJSONString()); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/ReportCardMapper.java b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/ReportCardMapper.java index c7566b88d..e5abf271e 100644 --- a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/ReportCardMapper.java +++ b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/ReportCardMapper.java @@ -1,11 +1,15 @@ package org.avni.server.mapper.dashboard; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.avni.server.domain.CHSBaseEntity; import org.avni.server.domain.ReportCard; import org.avni.server.service.CardService; +import org.avni.server.util.ObjectMapperSingleton; import org.avni.server.web.contract.EncounterTypeContract; import org.avni.server.web.contract.ProgramContract; import org.avni.server.web.contract.ReportCardContract; +import org.avni.server.web.contract.ValueUnit; import org.avni.server.web.request.StandardReportCardTypeContract; import org.avni.server.web.request.SubjectTypeContract; import org.avni.server.web.response.reports.ReportCardBundleContract; @@ -33,6 +37,9 @@ public ReportCardWebResponse toWebResponse(ReportCard card) { response.setStandardReportCardInputSubjectTypes(reportCardService.getStandardReportCardInputSubjectTypes(card).stream().map(SubjectTypeContract::createBasic).collect(Collectors.toList())); response.setStandardReportCardInputPrograms(reportCardService.getStandardReportCardInputPrograms(card).stream().map(ProgramContract::createBasic).collect(Collectors.toList())); response.setStandardReportCardInputEncounterTypes(reportCardService.getStandardReportCardInputEncounterTypes(card).stream().map(EncounterTypeContract::createBasic).collect(Collectors.toList())); + if (card.getStandardReportCardInputRecentDuration() != null) { + response.setStandardReportCardInputRecentDuration(buildDurationForRecentTypeCards(card.getStandardReportCardInputRecentDuration())); + } return response; } @@ -57,6 +64,18 @@ public ReportCardBundleContract toBundle(ReportCard reportCard) { response.setStandardReportCardInputSubjectTypes(reportCardService.getStandardReportCardInputSubjectTypes(reportCard).stream().map(CHSBaseEntity::getUuid).collect(Collectors.toList())); response.setStandardReportCardInputPrograms(reportCardService.getStandardReportCardInputPrograms(reportCard).stream().map(CHSBaseEntity::getUuid).collect(Collectors.toList())); response.setStandardReportCardInputEncounterTypes(reportCardService.getStandardReportCardInputEncounterTypes(reportCard).stream().map(CHSBaseEntity::getUuid).collect(Collectors.toList())); + if (reportCard.getStandardReportCardInputRecentDuration() != null) { + response.setStandardReportCardInputRecentDuration(buildDurationForRecentTypeCards(reportCard.getStandardReportCardInputRecentDuration())); + } return response; } + + private ValueUnit buildDurationForRecentTypeCards(String recentDurationString) { + try { + ObjectMapper objectMapper = ObjectMapperSingleton.getObjectMapper(); + return objectMapper.readValue(recentDurationString, ValueUnit.class); + } catch (JsonProcessingException e) { + return null; + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/CardService.java b/avni-server-api/src/main/java/org/avni/server/service/CardService.java index 271e2dda8..38d3493b4 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CardService.java @@ -76,6 +76,7 @@ private void buildStandardReportCardType(ReportCardWebRequest reportCardWebReque throw new BadRequestError(String.format("StandardReportCardType with id %d doesn't exist", standardReportCardTypeId)); } reportCard.setStandardReportCardType(type); + buildStandardReportCardInputs(type, reportCardWebRequest, reportCard); } else { reportCard.setStandardReportCardType(null); } @@ -90,11 +91,25 @@ private void buildStandardReportCardType(ReportCardBundleRequest reportCardBundl throw new BadRequestError(String.format("StandardReportCardType with uuid %s doesn't exist", standardReportCardTypeUUID)); } reportCard.setStandardReportCardType(type); + buildStandardReportCardInputs(type, reportCardBundleRequest, reportCard); } else { reportCard.setStandardReportCardType(null); } } + private void buildStandardReportCardInputs(StandardReportCardType type, ReportCardRequest reportCardRequest, ReportCard card) { + card.setStandardReportCardInputSubjectTypes(reportCardRequest.getStandardReportCardInputSubjectTypes()); + card.setStandardReportCardInputPrograms(reportCardRequest.getStandardReportCardInputPrograms()); + card.setStandardReportCardInputEncounterTypes(reportCardRequest.getStandardReportCardInputEncounterTypes()); + + if (type.getName().toLowerCase().contains("recent") && reportCardRequest.getStandardReportCardInputRecentDuration() == null) { + throw new BadRequestError("Recent Duration required for Recent type Standard Report cards"); + } + if (type.getName().toLowerCase().contains("recent")) { + card.setStandardReportCardInputRecentDuration(reportCardRequest.getStandardReportCardInputRecentDuration()); + } + } + private void buildCard(ReportCardRequest reportCardRequest, ReportCard card) { card.setName(reportCardRequest.getName()); card.setColour(reportCardRequest.getColor()); @@ -109,10 +124,6 @@ private void buildCard(ReportCardRequest reportCardRequest, ReportCard card) { ReportCard.INT_CONSTANT_DEFAULT_COUNT_OF_CARDS, ReportCard.INT_CONSTANT_MAX_COUNT_OF_CARDS)); } card.setCountOfCards(reportCardRequest.getCount()); - - card.setStandardReportCardInputSubjectTypes(reportCardRequest.getStandardReportCardInputSubjectTypes()); - card.setStandardReportCardInputPrograms(reportCardRequest.getStandardReportCardInputPrograms()); - card.setStandardReportCardInputEncounterTypes(reportCardRequest.getStandardReportCardInputEncounterTypes()); } private void assertNewNameIsUnique(String newName, String oldName) { diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/ValueUnit.java b/avni-server-api/src/main/java/org/avni/server/web/contract/ValueUnit.java new file mode 100644 index 000000000..05b81b2d8 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/ValueUnit.java @@ -0,0 +1,43 @@ +package org.avni.server.web.contract; + +import java.io.Serializable; +import java.util.LinkedHashMap; + +public class ValueUnit implements Serializable { + private String value; + private String unit; + + public ValueUnit() {} + + public ValueUnit(String valueUnit) {} + + public ValueUnit(String value, String unit) { + this.value = value; + this.unit = unit; + } + + public ValueUnit(LinkedHashMap valueUnit) { + this.value = valueUnit.get("value"); + this.unit = valueUnit.get("unit"); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public String toJSONString() { + return String.format("{\"%s\":\"%s\", \"%s\":\"%s\"}", "value", value, "unit", unit); + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/reports/ReportCardRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/reports/ReportCardRequest.java index c65a778d7..757e8a043 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/reports/ReportCardRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/reports/ReportCardRequest.java @@ -1,6 +1,7 @@ package org.avni.server.web.request.reports; import org.avni.server.web.contract.ReportCardContract; +import org.avni.server.web.contract.ValueUnit; import java.util.List; @@ -8,6 +9,7 @@ public abstract class ReportCardRequest extends ReportCardContract { private List standardReportCardInputSubjectTypes; private List standardReportCardInputPrograms; private List standardReportCardInputEncounterTypes; + private ValueUnit standardReportCardInputRecentDuration; public List getStandardReportCardInputSubjectTypes() { return standardReportCardInputSubjectTypes; @@ -32,4 +34,12 @@ public List getStandardReportCardInputEncounterTypes() { public void setStandardReportCardInputEncounterTypes(List standardReportCardInputEncounterTypes) { this.standardReportCardInputEncounterTypes = standardReportCardInputEncounterTypes; } + + public ValueUnit getStandardReportCardInputRecentDuration() { + return standardReportCardInputRecentDuration; + } + + public void setStandardReportCardInputRecentDuration(ValueUnit standardReportCardInputRecentDuration) { + this.standardReportCardInputRecentDuration = standardReportCardInputRecentDuration; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardBundleContract.java b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardBundleContract.java index de3386093..2b2b1e6d0 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardBundleContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardBundleContract.java @@ -1,6 +1,7 @@ package org.avni.server.web.response.reports; import org.avni.server.web.contract.ReportCardContract; +import org.avni.server.web.contract.ValueUnit; import java.util.ArrayList; import java.util.List; @@ -10,6 +11,7 @@ public class ReportCardBundleContract extends ReportCardContract { private List standardReportCardInputSubjectTypes = new ArrayList<>(); private List standardReportCardInputPrograms = new ArrayList<>(); private List standardReportCardInputEncounterTypes = new ArrayList<>(); + private ValueUnit standardReportCardInputRecentDuration = null; public String getStandardReportCardType() { return standardReportCardType; @@ -42,4 +44,8 @@ public List getStandardReportCardInputEncounterTypes() { public void setStandardReportCardInputEncounterTypes(List standardReportCardInputEncounterTypes) { this.standardReportCardInputEncounterTypes = standardReportCardInputEncounterTypes; } + + public void setStandardReportCardInputRecentDuration(ValueUnit standardReportCardInputRecentDuration) { + this.standardReportCardInputRecentDuration = standardReportCardInputRecentDuration; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardWebResponse.java b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardWebResponse.java index 6c3c28cfd..31008d640 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardWebResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardWebResponse.java @@ -3,6 +3,7 @@ import org.avni.server.web.contract.EncounterTypeContract; import org.avni.server.web.contract.ProgramContract; import org.avni.server.web.contract.ReportCardContract; +import org.avni.server.web.contract.ValueUnit; import org.avni.server.web.request.StandardReportCardTypeContract; import org.avni.server.web.request.SubjectTypeContract; @@ -15,6 +16,7 @@ public class ReportCardWebResponse extends ReportCardContract { private List standardReportCardInputSubjectTypes = new ArrayList<>(); private List standardReportCardInputPrograms = new ArrayList<>(); private List standardReportCardInputEncounterTypes = new ArrayList<>(); + private ValueUnit standardReportCardInputRecentDuration = null; public List getStandardReportCardInputSubjectTypes() { return standardReportCardInputSubjectTypes; @@ -47,4 +49,12 @@ public StandardReportCardTypeContract getStandardReportCardType() { public void setStandardReportCardType(StandardReportCardTypeContract standardReportCardType) { this.standardReportCardType = standardReportCardType; } + + public ValueUnit getStandardReportCardInputRecentDuration() { + return standardReportCardInputRecentDuration; + } + + public void setStandardReportCardInputRecentDuration(ValueUnit standardReportCardInputRecentDuration) { + this.standardReportCardInputRecentDuration = standardReportCardInputRecentDuration; + } } diff --git a/avni-server-api/src/main/resources/db/migration/V1_342__UpdateStandardCardTypeNames.sql b/avni-server-api/src/main/resources/db/migration/V1_342__UpdateStandardCardTypeNames.sql new file mode 100644 index 000000000..f2ead5142 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_342__UpdateStandardCardTypeNames.sql @@ -0,0 +1,9 @@ +UPDATE standard_report_card_type +SET name = 'Recent registrations', description = 'Recent registrations', last_modified_date_time = current_timestamp + WHERE name = 'Last 24 hours registrations'; +UPDATE standard_report_card_type +SET name = 'Recent enrolments', description = 'Recent enrolments', last_modified_date_time = current_timestamp + WHERE name = 'Last 24 hours enrolments'; +UPDATE standard_report_card_type +SET name = 'Recent visits', description = 'Recent visits', last_modified_date_time = current_timestamp + WHERE name = 'Last 24 hours visits'; From 76329caa924eb3c34872be22ae55d89ac6639c79 Mon Sep 17 00:00:00 2001 From: Joy A Date: Mon, 1 Jul 2024 14:40:33 +0530 Subject: [PATCH 004/129] avniproject/avni-webapp#1263 | Handle recent duration input for standard report cards in bundle --- .../mapper/dashboard/ReportCardMapper.java | 19 +-------- .../org/avni/server/service/CardService.java | 37 +++++++++++----- .../reports/ReportCardBundleRequest.java | 42 ++++++++++++++++++- .../reports/ReportCardBundleContract.java | 9 ++-- 4 files changed, 75 insertions(+), 32 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/ReportCardMapper.java b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/ReportCardMapper.java index e5abf271e..36a268872 100644 --- a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/ReportCardMapper.java +++ b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/ReportCardMapper.java @@ -1,15 +1,11 @@ package org.avni.server.mapper.dashboard; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.avni.server.domain.CHSBaseEntity; import org.avni.server.domain.ReportCard; import org.avni.server.service.CardService; -import org.avni.server.util.ObjectMapperSingleton; import org.avni.server.web.contract.EncounterTypeContract; import org.avni.server.web.contract.ProgramContract; import org.avni.server.web.contract.ReportCardContract; -import org.avni.server.web.contract.ValueUnit; import org.avni.server.web.request.StandardReportCardTypeContract; import org.avni.server.web.request.SubjectTypeContract; import org.avni.server.web.response.reports.ReportCardBundleContract; @@ -37,9 +33,7 @@ public ReportCardWebResponse toWebResponse(ReportCard card) { response.setStandardReportCardInputSubjectTypes(reportCardService.getStandardReportCardInputSubjectTypes(card).stream().map(SubjectTypeContract::createBasic).collect(Collectors.toList())); response.setStandardReportCardInputPrograms(reportCardService.getStandardReportCardInputPrograms(card).stream().map(ProgramContract::createBasic).collect(Collectors.toList())); response.setStandardReportCardInputEncounterTypes(reportCardService.getStandardReportCardInputEncounterTypes(card).stream().map(EncounterTypeContract::createBasic).collect(Collectors.toList())); - if (card.getStandardReportCardInputRecentDuration() != null) { - response.setStandardReportCardInputRecentDuration(buildDurationForRecentTypeCards(card.getStandardReportCardInputRecentDuration())); - } + response.setStandardReportCardInputRecentDuration(reportCardService.buildDurationForRecentTypeCards(card.getStandardReportCardInputRecentDuration())); return response; } @@ -65,17 +59,8 @@ public ReportCardBundleContract toBundle(ReportCard reportCard) { response.setStandardReportCardInputPrograms(reportCardService.getStandardReportCardInputPrograms(reportCard).stream().map(CHSBaseEntity::getUuid).collect(Collectors.toList())); response.setStandardReportCardInputEncounterTypes(reportCardService.getStandardReportCardInputEncounterTypes(reportCard).stream().map(CHSBaseEntity::getUuid).collect(Collectors.toList())); if (reportCard.getStandardReportCardInputRecentDuration() != null) { - response.setStandardReportCardInputRecentDuration(buildDurationForRecentTypeCards(reportCard.getStandardReportCardInputRecentDuration())); + response.setStandardReportCardInputRecentDuration(reportCard.getStandardReportCardInputRecentDuration()); } return response; } - - private ValueUnit buildDurationForRecentTypeCards(String recentDurationString) { - try { - ObjectMapper objectMapper = ObjectMapperSingleton.getObjectMapper(); - return objectMapper.readValue(recentDurationString, ValueUnit.class); - } catch (JsonProcessingException e) { - return null; - } - } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/CardService.java b/avni-server-api/src/main/java/org/avni/server/service/CardService.java index 38d3493b4..484c51027 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CardService.java @@ -1,15 +1,19 @@ package org.avni.server.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.avni.server.dao.*; import org.avni.server.domain.*; import org.avni.server.util.BadRequestError; +import org.avni.server.util.ObjectMapperSingleton; +import org.avni.server.web.contract.ReportCardContract; +import org.avni.server.web.contract.ValueUnit; import org.avni.server.web.request.reports.ReportCardBundleRequest; -import org.avni.server.web.request.reports.ReportCardRequest; import org.avni.server.web.request.reports.ReportCardWebRequest; +import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.joda.time.DateTime; import java.util.List; @Service @@ -76,7 +80,7 @@ private void buildStandardReportCardType(ReportCardWebRequest reportCardWebReque throw new BadRequestError(String.format("StandardReportCardType with id %d doesn't exist", standardReportCardTypeId)); } reportCard.setStandardReportCardType(type); - buildStandardReportCardInputs(type, reportCardWebRequest, reportCard); + buildStandardReportCardInputs(type, reportCardWebRequest.getStandardReportCardInputSubjectTypes(), reportCardWebRequest.getStandardReportCardInputPrograms(), reportCardWebRequest.getStandardReportCardInputPrograms(), reportCardWebRequest.getStandardReportCardInputRecentDuration(), reportCard); } else { reportCard.setStandardReportCardType(null); } @@ -91,26 +95,27 @@ private void buildStandardReportCardType(ReportCardBundleRequest reportCardBundl throw new BadRequestError(String.format("StandardReportCardType with uuid %s doesn't exist", standardReportCardTypeUUID)); } reportCard.setStandardReportCardType(type); - buildStandardReportCardInputs(type, reportCardBundleRequest, reportCard); + ValueUnit recentDuration = buildDurationForRecentTypeCards(reportCardBundleRequest.getStandardReportCardInputRecentDuration()); + buildStandardReportCardInputs(type, reportCardBundleRequest.getStandardReportCardInputSubjectTypes(), reportCardBundleRequest.getStandardReportCardInputPrograms(), reportCardBundleRequest.getStandardReportCardInputEncounterTypes(), recentDuration, reportCard); } else { reportCard.setStandardReportCardType(null); } } - private void buildStandardReportCardInputs(StandardReportCardType type, ReportCardRequest reportCardRequest, ReportCard card) { - card.setStandardReportCardInputSubjectTypes(reportCardRequest.getStandardReportCardInputSubjectTypes()); - card.setStandardReportCardInputPrograms(reportCardRequest.getStandardReportCardInputPrograms()); - card.setStandardReportCardInputEncounterTypes(reportCardRequest.getStandardReportCardInputEncounterTypes()); + private void buildStandardReportCardInputs(StandardReportCardType type, List srciSubjectTypes, List srciPrograms, List srciEncounterTypes, ValueUnit srciRecentDuration, ReportCard card) { + card.setStandardReportCardInputSubjectTypes(srciSubjectTypes); + card.setStandardReportCardInputPrograms(srciPrograms); + card.setStandardReportCardInputEncounterTypes(srciEncounterTypes); - if (type.getName().toLowerCase().contains("recent") && reportCardRequest.getStandardReportCardInputRecentDuration() == null) { + if (type.getName().toLowerCase().contains("recent") && srciRecentDuration == null) { throw new BadRequestError("Recent Duration required for Recent type Standard Report cards"); } if (type.getName().toLowerCase().contains("recent")) { - card.setStandardReportCardInputRecentDuration(reportCardRequest.getStandardReportCardInputRecentDuration()); + card.setStandardReportCardInputRecentDuration(srciRecentDuration); } } - private void buildCard(ReportCardRequest reportCardRequest, ReportCard card) { + private void buildCard(ReportCardContract reportCardRequest, ReportCard card) { card.setName(reportCardRequest.getName()); card.setColour(reportCardRequest.getColor()); card.setDescription(reportCardRequest.getDescription()); @@ -126,6 +131,16 @@ private void buildCard(ReportCardRequest reportCardRequest, ReportCard card) { card.setCountOfCards(reportCardRequest.getCount()); } + public ValueUnit buildDurationForRecentTypeCards(String recentDurationString) { + if (recentDurationString == null) return null; + try { + ObjectMapper objectMapper = ObjectMapperSingleton.getObjectMapper(); + return objectMapper.readValue(recentDurationString, ValueUnit.class); + } catch (JsonProcessingException e) { + return null; + } + } + private void assertNewNameIsUnique(String newName, String oldName) { if (!newName.equals(oldName)) { assertNoExistingCardWithName(newName); diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/reports/ReportCardBundleRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/reports/ReportCardBundleRequest.java index 568cd53cf..47bbcde9f 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/reports/ReportCardBundleRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/reports/ReportCardBundleRequest.java @@ -1,7 +1,15 @@ package org.avni.server.web.request.reports; -public class ReportCardBundleRequest extends ReportCardRequest { +import org.avni.server.web.contract.ReportCardContract; + +import java.util.List; + +public class ReportCardBundleRequest extends ReportCardContract { private String standardReportCardType; + private List standardReportCardInputSubjectTypes; + private List standardReportCardInputPrograms; + private List standardReportCardInputEncounterTypes; + private String standardReportCardInputRecentDuration; public String getStandardReportCardType() { return standardReportCardType; @@ -10,4 +18,36 @@ public String getStandardReportCardType() { public void setStandardReportCardType(String standardReportCardType) { this.standardReportCardType = standardReportCardType; } + + public List getStandardReportCardInputSubjectTypes() { + return standardReportCardInputSubjectTypes; + } + + public void setStandardReportCardInputSubjectTypes(List standardReportCardInputSubjectTypes) { + this.standardReportCardInputSubjectTypes = standardReportCardInputSubjectTypes; + } + + public List getStandardReportCardInputPrograms() { + return standardReportCardInputPrograms; + } + + public void setStandardReportCardInputPrograms(List standardReportCardInputPrograms) { + this.standardReportCardInputPrograms = standardReportCardInputPrograms; + } + + public List getStandardReportCardInputEncounterTypes() { + return standardReportCardInputEncounterTypes; + } + + public void setStandardReportCardInputEncounterTypes(List standardReportCardInputEncounterTypes) { + this.standardReportCardInputEncounterTypes = standardReportCardInputEncounterTypes; + } + + public String getStandardReportCardInputRecentDuration() { + return standardReportCardInputRecentDuration; + } + + public void setStandardReportCardInputRecentDuration(String standardReportCardInputRecentDuration) { + this.standardReportCardInputRecentDuration = standardReportCardInputRecentDuration; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardBundleContract.java b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardBundleContract.java index 2b2b1e6d0..22893b7a1 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardBundleContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ReportCardBundleContract.java @@ -1,7 +1,6 @@ package org.avni.server.web.response.reports; import org.avni.server.web.contract.ReportCardContract; -import org.avni.server.web.contract.ValueUnit; import java.util.ArrayList; import java.util.List; @@ -11,7 +10,7 @@ public class ReportCardBundleContract extends ReportCardContract { private List standardReportCardInputSubjectTypes = new ArrayList<>(); private List standardReportCardInputPrograms = new ArrayList<>(); private List standardReportCardInputEncounterTypes = new ArrayList<>(); - private ValueUnit standardReportCardInputRecentDuration = null; + private String standardReportCardInputRecentDuration = null; public String getStandardReportCardType() { return standardReportCardType; @@ -45,7 +44,11 @@ public void setStandardReportCardInputEncounterTypes(List standardReport this.standardReportCardInputEncounterTypes = standardReportCardInputEncounterTypes; } - public void setStandardReportCardInputRecentDuration(ValueUnit standardReportCardInputRecentDuration) { + public String getStandardReportCardInputRecentDuration() { + return standardReportCardInputRecentDuration; + } + + public void setStandardReportCardInputRecentDuration(String standardReportCardInputRecentDuration) { this.standardReportCardInputRecentDuration = standardReportCardInputRecentDuration; } } From 51707e57ea449192e9e937e7c3f69af106dad66b Mon Sep 17 00:00:00 2001 From: Joy A Date: Tue, 2 Jul 2024 14:22:37 +0530 Subject: [PATCH 005/129] avniproject/avni-webapp#1263 | Use object mapper to serialize ValueUnit to JSON Propagate error if occurs while (de)serializing ValueUnit --- .../java/org/avni/server/service/CardService.java | 7 +++---- .../org/avni/server/web/contract/ValueUnit.java | 15 ++++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/CardService.java b/avni-server-api/src/main/java/org/avni/server/service/CardService.java index 484c51027..68ae0a4e3 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CardService.java @@ -1,7 +1,6 @@ package org.avni.server.service; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.avni.server.dao.*; import org.avni.server.domain.*; import org.avni.server.util.BadRequestError; @@ -134,11 +133,11 @@ private void buildCard(ReportCardContract reportCardRequest, ReportCard card) { public ValueUnit buildDurationForRecentTypeCards(String recentDurationString) { if (recentDurationString == null) return null; try { - ObjectMapper objectMapper = ObjectMapperSingleton.getObjectMapper(); - return objectMapper.readValue(recentDurationString, ValueUnit.class); + return ObjectMapperSingleton.getObjectMapper().readValue(recentDurationString, ValueUnit.class); } catch (JsonProcessingException e) { - return null; + throw new RuntimeException(e); } + } private void assertNewNameIsUnique(String newName, String oldName) { diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/ValueUnit.java b/avni-server-api/src/main/java/org/avni/server/web/contract/ValueUnit.java index 05b81b2d8..8c959e783 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/contract/ValueUnit.java +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/ValueUnit.java @@ -1,7 +1,9 @@ package org.avni.server.web.contract; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.avni.server.util.ObjectMapperSingleton; + import java.io.Serializable; -import java.util.LinkedHashMap; public class ValueUnit implements Serializable { private String value; @@ -16,11 +18,6 @@ public ValueUnit(String value, String unit) { this.unit = unit; } - public ValueUnit(LinkedHashMap valueUnit) { - this.value = valueUnit.get("value"); - this.unit = valueUnit.get("unit"); - } - public String getValue() { return value; } @@ -38,6 +35,10 @@ public void setUnit(String unit) { } public String toJSONString() { - return String.format("{\"%s\":\"%s\", \"%s\":\"%s\"}", "value", value, "unit", unit); + try { + return ObjectMapperSingleton.getObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } } From 4a637334cc9b7bf47c3802b1aa4274882fd1d848 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 2 Jul 2024 16:22:50 +0530 Subject: [PATCH 006/129] avniproject/avni-client#1441 - support for as on date --- .../org/avni/server/domain/app/dashboard/DashboardFilter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/app/dashboard/DashboardFilter.java b/avni-server-api/src/main/java/org/avni/server/domain/app/dashboard/DashboardFilter.java index 179686dd2..5e4144530 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/app/dashboard/DashboardFilter.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/app/dashboard/DashboardFilter.java @@ -150,6 +150,7 @@ public static enum FilterType { GroupSubject, Address, Concept, - SubjectType; + SubjectType, + AsOnDate; } } From 9f6812ba8c14cd4d7b653e2f600f53e0232611b5 Mon Sep 17 00:00:00 2001 From: sachinkadam Date: Tue, 2 Jul 2024 22:23:44 +0530 Subject: [PATCH 007/129] avniproject/avni-webapp#1209 | Rename the dashboard if it is voided to support same name --- .../src/main/java/org/avni/server/service/DashboardService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java index f8344ccbf..cd4ee8e32 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java @@ -4,6 +4,7 @@ import org.avni.server.domain.*; import org.avni.server.domain.app.dashboard.DashboardFilter; import org.avni.server.util.BadRequestError; +import org.avni.server.util.ReactAdminUtil; import org.avni.server.web.contract.reports.DashboardBundleContract; import org.avni.server.web.contract.reports.DashboardSectionBundleContract; import org.avni.server.web.contract.reports.DashboardSectionCardMappingBundleContract; @@ -93,6 +94,7 @@ public Dashboard editDashboard(DashboardWebRequest dashboardRequest, Long dashbo public void deleteDashboard(Dashboard dashboard) { dashboard.setVoided(true); + dashboard.setName((ReactAdminUtil.getVoidedName(dashboard.getName(), dashboard.getId()))); dashboardRepository.save(dashboard); } From 05160d9821d9023cad1159f3d3270a9a33641efb Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 3 Jul 2024 14:30:30 +0530 Subject: [PATCH 008/129] avniproject/avni-client#1437 - fix for program uuid was getting enc type uuid. --- .../java/org/avni/server/domain/ReportCard.java | 6 +----- .../org/avni/server/service/CardService.java | 16 +++++++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java b/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java index f958bbffa..bbd1da858 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java @@ -144,10 +144,6 @@ private List safeGetterForStandardReportCardInput(String key) { return standardReportCardInput == null ? new ArrayList() : standardReportCardInput.getList(key); } - private String getterForStandardReportCardInputString(String key) { - return standardReportCardInput.getString(key); - } - private void safeSetterForStandardReportCardInput(String key, List value) { if (standardReportCardInput == null) { standardReportCardInput = new JsonObject(new HashMap<>()); @@ -163,7 +159,7 @@ private void safeSetterForStandardReportCardInput(String key, String value) { } public String getStandardReportCardInputRecentDuration() { - return getterForStandardReportCardInputString("recentDuration"); + return standardReportCardInput.getString("recentDuration"); } public void setStandardReportCardInputRecentDuration(ValueUnit standardReportCardInputRecentDuration) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/CardService.java b/avni-server-api/src/main/java/org/avni/server/service/CardService.java index 68ae0a4e3..671993cd6 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CardService.java @@ -79,7 +79,9 @@ private void buildStandardReportCardType(ReportCardWebRequest reportCardWebReque throw new BadRequestError(String.format("StandardReportCardType with id %d doesn't exist", standardReportCardTypeId)); } reportCard.setStandardReportCardType(type); - buildStandardReportCardInputs(type, reportCardWebRequest.getStandardReportCardInputSubjectTypes(), reportCardWebRequest.getStandardReportCardInputPrograms(), reportCardWebRequest.getStandardReportCardInputPrograms(), reportCardWebRequest.getStandardReportCardInputRecentDuration(), reportCard); + buildStandardReportCardInputs(type, reportCardWebRequest.getStandardReportCardInputSubjectTypes(), + reportCardWebRequest.getStandardReportCardInputPrograms(), + reportCardWebRequest.getStandardReportCardInputEncounterTypes(), reportCardWebRequest.getStandardReportCardInputRecentDuration(), reportCard); } else { reportCard.setStandardReportCardType(null); } @@ -101,16 +103,16 @@ private void buildStandardReportCardType(ReportCardBundleRequest reportCardBundl } } - private void buildStandardReportCardInputs(StandardReportCardType type, List srciSubjectTypes, List srciPrograms, List srciEncounterTypes, ValueUnit srciRecentDuration, ReportCard card) { - card.setStandardReportCardInputSubjectTypes(srciSubjectTypes); - card.setStandardReportCardInputPrograms(srciPrograms); - card.setStandardReportCardInputEncounterTypes(srciEncounterTypes); + private void buildStandardReportCardInputs(StandardReportCardType type, List subjectTypes, List programs, List encounterTypes, ValueUnit recentDuration, ReportCard card) { + card.setStandardReportCardInputSubjectTypes(subjectTypes); + card.setStandardReportCardInputPrograms(programs); + card.setStandardReportCardInputEncounterTypes(encounterTypes); - if (type.getName().toLowerCase().contains("recent") && srciRecentDuration == null) { + if (type.getName().toLowerCase().contains("recent") && recentDuration == null) { throw new BadRequestError("Recent Duration required for Recent type Standard Report cards"); } if (type.getName().toLowerCase().contains("recent")) { - card.setStandardReportCardInputRecentDuration(srciRecentDuration); + card.setStandardReportCardInputRecentDuration(recentDuration); } } From f04d7dbdd1599e9e938efa4b9dacf88e4053393b Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 3 Jul 2024 18:11:50 +0530 Subject: [PATCH 009/129] #744 | Init isPrimaryDashboard and isSecondaryDashboard flags for UserGroupsDashboards during bundle upload --- .../java/org/avni/server/service/GroupDashboardService.java | 2 ++ avni-server-api/src/main/resources/database/usefulQueries.sql | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java index 6e2cc04b4..416d32740 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java @@ -68,6 +68,8 @@ public void saveFromBundle(List request) { groupDashboard.setDashboard(dashboard); groupDashboard.setGroup(group); groupDashboard.setOrganisationId(organisationId); + groupDashboard.setPrimaryDashboard(contract.isPrimaryDashboard()); + groupDashboard.setSecondaryDashboard(contract.isSecondaryDashboard()); groupDashboards.add(groupDashboard); } groupDashboardRepository.saveAll(groupDashboards); diff --git a/avni-server-api/src/main/resources/database/usefulQueries.sql b/avni-server-api/src/main/resources/database/usefulQueries.sql index aaec7e17d..faa6d1eeb 100644 --- a/avni-server-api/src/main/resources/database/usefulQueries.sql +++ b/avni-server-api/src/main/resources/database/usefulQueries.sql @@ -419,7 +419,8 @@ select pid as process_id, application_name, backend_start, state, - state_change + state_change, + query from pg_stat_activity; From a6178e399efb4d521fe2ebe2ec544f98e90b9cfd Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 4 Jul 2024 12:52:24 +0530 Subject: [PATCH 010/129] #740 - fix for new user --- .../src/main/java/org/avni/server/dao/UserRepository.java | 3 +++ .../src/main/java/org/avni/server/service/UserService.java | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java index e56351ee0..b826400ac 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java @@ -140,4 +140,7 @@ default User getDefaultSuperAdmin() { @Query(value = "select * from users where lower(users.settings->>'idPrefix') = lower(:prefix) and id <> :exceptUserId", nativeQuery = true) List getUsersWithSameIdPrefix(String prefix, long exceptUserId); + + @Query(value = "select * from users where lower(users.settings->>'idPrefix') = lower(:prefix)", nativeQuery = true) + List getUsersWithSameIdPrefix(String prefix); } diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserService.java b/avni-server-api/src/main/java/org/avni/server/service/UserService.java index 248a3b047..f9d13ee78 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserService.java @@ -52,8 +52,8 @@ public User save(User user) { String idPrefix = UserSettings.getIdPrefix(user.getSettings()); if (StringUtils.hasLength(idPrefix)) { synchronized (String.format("%d-USER-ID-PREFIX-%s", user.getOrganisationId(), idPrefix).intern()) { - List usersWithSameIdPrefix = userRepository.getUsersWithSameIdPrefix(idPrefix, user.getId()); - if (usersWithSameIdPrefix.size() == 0) { + List usersWithSameIdPrefix = user.isNew() ? userRepository.getUsersWithSameIdPrefix(idPrefix) : userRepository.getUsersWithSameIdPrefix(idPrefix, user.getId()); + if (usersWithSameIdPrefix.isEmpty()) { return createUpdateUser(user); } else { throw new ValidationException(String.format("There is another user %s with same prefix: %s", usersWithSameIdPrefix.get(0).getUsername(), idPrefix)); From e31c174c1b8d1c0c120a1c849cd82e2c7f8a238c Mon Sep 17 00:00:00 2001 From: Vinay Venu Date: Thu, 4 Jul 2024 15:46:10 +0530 Subject: [PATCH 011/129] avniproject/avni-webapp#763 | Fix errors when there are multiple organisations with same uuid, and when there are no results --- .../org/avni/server/dao/SubjectSearchRepository.java | 4 ++-- .../avni/server/service/IndividualSearchService.java | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/SubjectSearchRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/SubjectSearchRepository.java index cf162f544..306e26900 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/SubjectSearchRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/SubjectSearchRepository.java @@ -31,8 +31,8 @@ public SubjectSearchRepository(EntityManager entityManager) { @Transactional public List> search(SubjectSearchRequest searchRequest, SearchBuilder searchBuilder) { try { - setRoleToNone(); SqlQuery query = searchBuilder.getSQLResultQuery(searchRequest); + setRoleToNone(); logger.debug("Executing query: " + query.getSql()); logger.debug("Parameters: " + query.getParameters()); Query sql = entityManager.createNativeQuery(query.getSql()); @@ -50,8 +50,8 @@ public List> search(SubjectSearchRequest searchRequest, Searc @Transactional public BigInteger getTotalCount(SubjectSearchRequest searchRequest, SearchBuilder searchBuilder) { try { - setRoleToNone(); SqlQuery query = searchBuilder.getSQLCountQuery(searchRequest); + setRoleToNone(); Query sql = entityManager.createNativeQuery(query.getSql()); query.getParameters().forEach((name, value) -> { sql.setParameter(name, value); diff --git a/avni-server-api/src/main/java/org/avni/server/service/IndividualSearchService.java b/avni-server-api/src/main/java/org/avni/server/service/IndividualSearchService.java index d64a69e0e..aa3f18bd3 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IndividualSearchService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IndividualSearchService.java @@ -9,9 +9,7 @@ import org.springframework.stereotype.Service; import java.math.BigInteger; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Service @@ -41,7 +39,11 @@ private LinkedHashMap constructIndividual(List addressIds = individualList.stream() .map(individualRecord -> ((BigInteger) individualRecord.get("addressId")).longValue()) .collect(Collectors.toList()); - List searchSubjectEnrolledPrograms = programEnrolmentRepository.findActiveEnrolmentsByIndividualIds(individualIds); + + List searchSubjectEnrolledPrograms = individualIds.size() > 0 ? + programEnrolmentRepository.findActiveEnrolmentsByIndividualIds(individualIds) : + Collections.emptyList(); + Map titleLineages = addressLevelService.getTitleLineages(addressIds); List> listOfRecords = individualList.stream() From adaab392aa7c5f81fc189362b03e67a6c0bc0c36 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 5 Jul 2024 12:29:42 +0530 Subject: [PATCH 012/129] avniproject/avni-webapp#1255 - Row trims everything --- .../batch/csv/creator/LocationCreator.java | 3 +-- .../avni/server/importer/batch/model/Row.java | 24 +++++++++++++++++-- .../avni/server/service/ExportS3Service.java | 1 - .../server/importer/batch/model/RowTest.java | 16 +++++++++---- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/LocationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/LocationCreator.java index d2e3a543a..2245b221a 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/LocationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/LocationCreator.java @@ -9,8 +9,7 @@ import java.util.List; public class LocationCreator { - - private static Logger logger = LoggerFactory.getLogger(LocationCreator.class); + private static final Logger logger = LoggerFactory.getLogger(LocationCreator.class); public Point getLocation(Row row, String header, List errorMsgs) { try { diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/model/Row.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/model/Row.java index cd600790a..d5123dbe8 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/model/Row.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/model/Row.java @@ -8,7 +8,6 @@ import static java.lang.String.format; public class Row extends HashMap { - public static final Pattern TRUE_VALUE = Pattern.compile("y|yes|true|1", Pattern.CASE_INSENSITIVE); private final String[] headers; private final String[] values; @@ -19,8 +18,29 @@ public Row(String[] headers, String[] values) { IntStream.range(0, values.length).forEach(index -> this.put(headers[index], values[index].trim())); } + private String nullSafeTrim(String s) { + if (s == null) { + return null; + } + return s.trim(); + } + public String[] getHeaders() { - return headers; + return Arrays.stream(headers).map(this::nullSafeTrim).toArray(String[]::new); + } + + @Override + public String get(Object key) { + String k = nullSafeTrim((String) key); + String s = super.get(k); + return this.nullSafeTrim(s); + } + + @Override + public String getOrDefault(Object key, String defaultValue) { + String k = nullSafeTrim((String) key); + String s = super.getOrDefault(k, defaultValue); + return this.nullSafeTrim(s); } @Override diff --git a/avni-server-api/src/main/java/org/avni/server/service/ExportS3Service.java b/avni-server-api/src/main/java/org/avni/server/service/ExportS3Service.java index e0faee28e..f9ecce29d 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ExportS3Service.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ExportS3Service.java @@ -1,6 +1,5 @@ package org.avni.server.service; -import org.avni.server.domain.UserContext; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.util.S3File; import org.avni.server.util.S3FileType; diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/model/RowTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/model/RowTest.java index 0df2729c2..e8d5d0b92 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/model/RowTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/model/RowTest.java @@ -1,19 +1,27 @@ package org.avni.server.importer.batch.model; -import org.avni.server.importer.batch.model.Row; import org.junit.Test; import static org.junit.Assert.assertEquals; public class RowTest { - @Test public void toStringShouldSerialiseProperly() throws Exception { String[] headers = {"A", "B"}; assertEquals("\"AA\",\"\"", new Row(headers, new String[]{"AA"}).toString()); - assertEquals("\"AA\",\"BB\"", new Row(headers, new String[]{"AA", "BB"}).toString()); - assertEquals("\"AB, CD\",\"BB, EE\"", new Row(headers, new String[]{"AB, CD", "BB, EE"}).toString()); } + + @Test + public void trimHeadersAndValues() { + String[] headers = {"A", "B"}; + Row row = new Row(headers, new String[]{" AA ", " BB"}); + assertEquals("A", row.getHeaders()[0]); + assertEquals("B", row.getHeaders()[1]); + assertEquals("AA", row.get("A")); + assertEquals("BB", row.get("B")); + assertEquals("AA", row.get("A ")); + assertEquals("BB", row.get(" B")); + } } From dacd0a13b7ebac2e1c87a8d941f5241dad278d66 Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 5 Jul 2024 14:42:27 +0530 Subject: [PATCH 013/129] #744 | Init User Group voided flag during bundle export and import --- .../src/main/java/org/avni/server/service/GroupsService.java | 1 + .../src/main/java/org/avni/server/web/request/GroupContract.java | 1 + 2 files changed, 2 insertions(+) diff --git a/avni-server-api/src/main/java/org/avni/server/service/GroupsService.java b/avni-server-api/src/main/java/org/avni/server/service/GroupsService.java index a94ae7dcb..743aa651f 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/GroupsService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/GroupsService.java @@ -31,6 +31,7 @@ public Group saveGroup(GroupContract groupContract, Organisation organisation) { } else { group = groupRepository.findByNameAndOrganisationId(Group.Everyone, organisation.getId()); } + group.setVoided(groupContract.isVoided()); group.setHasAllPrivileges(groupContract.isHasAllPrivileges()); return groupRepository.save(group); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/GroupContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/GroupContract.java index ddc2cbc1c..a38c278db 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/GroupContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/GroupContract.java @@ -17,6 +17,7 @@ public static GroupContract fromEntity(Group group) { groupContract.setHasAllPrivileges(group.isHasAllPrivileges()); groupContract.setNotEveryoneGroup(!group.isEveryone()); groupContract.setUuid(group.getUuid()); + groupContract.setVoided(group.isVoided()); return groupContract; } From d998af123b8fd168d2e4f17550848cd2a5b0c1eb Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 5 Jul 2024 17:33:01 +0530 Subject: [PATCH 014/129] #744 | Init GroupDashboard voided flag during bundle export and import --- .../main/java/org/avni/server/service/GroupDashboardService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java index 416d32740..1052bb7bc 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java @@ -70,6 +70,7 @@ public void saveFromBundle(List request) { groupDashboard.setOrganisationId(organisationId); groupDashboard.setPrimaryDashboard(contract.isPrimaryDashboard()); groupDashboard.setSecondaryDashboard(contract.isSecondaryDashboard()); + groupDashboard.setVoided(contract.isVoided()); groupDashboards.add(groupDashboard); } groupDashboardRepository.saveAll(groupDashboards); From e1c3f7f36d11b9d0a156f11209ec6533712eef38 Mon Sep 17 00:00:00 2001 From: Joy A Date: Wed, 10 Jul 2024 19:19:17 +0530 Subject: [PATCH 015/129] #721 | impl_version 1 for GroupPrivilege --- .../server/dao/GroupPrivilegeRepository.java | 14 ++++++- .../domain/accessControl/GroupPrivilege.java | 13 ++++++- .../server/service/OrganisationService.java | 2 + .../accessControl/GroupPrivilegeService.java | 37 +++++++++++++------ .../server/web/GroupPrivilegeController.java | 5 ++- .../V1_339_1__GroupPrivilegeConstraint.sql | 35 ++++++++++++++++++ .../service/builder/TestGroupService.java | 2 +- 7 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 avni-server-api/src/main/resources/db/migration/V1_339_1__GroupPrivilegeConstraint.sql diff --git a/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java index 2ac28ea55..d9e5c9954 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java @@ -1,6 +1,5 @@ package org.avni.server.dao; -import org.avni.server.domain.CHSEntity; import org.avni.server.domain.accessControl.GroupPrivilege; import org.joda.time.DateTime; import org.springframework.data.domain.Page; @@ -27,7 +26,7 @@ default GroupPrivilege findByNameIgnoreCase(String name) { throw new UnsupportedOperationException("No field 'name' in GroupPrivilege."); } - List findByGroup_Id(Long groupId); + List findByGroup_IdAndImplVersion(Long groupId, int implVersion); @Query(value = "select distinct gp.*\n" + "from group_privilege gp\n" + @@ -35,6 +34,7 @@ default GroupPrivilege findByNameIgnoreCase(String name) { " join privilege p on gp.privilege_id = p.id\n" + "where ug.user_id = :userId\n" + " and gp.is_voided = false\n" + + " and gp.impl_version = 1\n" + " and ug.is_voided = false\n" + " and allow = true", nativeQuery = true) List getAllAllowedPrivilegesForUser(Long userId); @@ -50,4 +50,14 @@ Page findBySubjectTypeIsNotNullAndLastModifiedDateTimeIsBetweenO default boolean existsByLastModifiedDateTimeGreaterThan(DateTime lastModifiedDateTime) { return existsByLastModifiedDateTimeGreaterThan(lastModifiedDateTime == null ? null : lastModifiedDateTime.toDate()); } + + default GroupPrivilege saveGroupPrivilege(GroupPrivilege groupPrivilege) { + groupPrivilege.setImplVersion(GroupPrivilege.IMPL_VERSION); + return this.save(groupPrivilege); + } + + default List saveAllGroupPrivileges(List groupPrivileges) { + groupPrivileges.forEach(gp -> gp.setImplVersion(GroupPrivilege.IMPL_VERSION)); + return this.saveAll(groupPrivileges); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/GroupPrivilege.java b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/GroupPrivilege.java index f2868a266..b7d8793f2 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/GroupPrivilege.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/GroupPrivilege.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.avni.server.domain.*; -import org.avni.server.domain.accessControl.Privilege; import org.hibernate.annotations.BatchSize; import javax.persistence.*; @@ -13,6 +12,7 @@ @JsonIgnoreProperties({"group", "privilege", "subjectType", "program", "programEncounterType", "encounterType", "checklistDetail"}) @BatchSize(size = 100) public class GroupPrivilege extends OrganisationAwareEntity { + public static final int IMPL_VERSION = 1; @NotNull @ManyToOne(fetch = FetchType.LAZY) @@ -46,6 +46,9 @@ public class GroupPrivilege extends OrganisationAwareEntity { private boolean allow; + @Column + private int implVersion; + public Group getGroup() { return group; } @@ -207,4 +210,12 @@ public String toString() { ", allow=" + allow + '}'; } + + public int getImplVersion() { + return implVersion; + } + + public void setImplVersion(int implVersion) { + this.implVersion = implVersion; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index 94e89a6e5..9bdc10c6d 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -21,6 +21,7 @@ import org.avni.server.dao.program.SubjectProgramEligibilityRepository; import org.avni.server.dao.task.TaskRepository; import org.avni.server.domain.*; +import org.avni.server.domain.accessControl.GroupPrivilege; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.importer.batch.model.BundleFolder; import org.avni.server.service.application.MenuItemService; @@ -366,6 +367,7 @@ public void addGroupsJson(ZipOutputStream zos) throws IOException { public void addGroupPrivilegeJson(ZipOutputStream zos) throws IOException { List groupPrivileges = groupPrivilegeRepository.findAll().stream() .filter(groupPrivilege -> !groupPrivilege.getGroup().isAdministrator()) + .filter(groupPrivilege -> groupPrivilege.getImplVersion() == GroupPrivilege.IMPL_VERSION) .map(GroupPrivilegeContractWeb::fromEntity).collect(Collectors.toList()); if (!groupPrivileges.isEmpty()) { addFileToZip(zos, "groupPrivilege.json", groupPrivileges); diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java index 9554482ee..8ed5f5ef7 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java @@ -21,6 +21,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; @Service public class GroupPrivilegeService implements NonScopeAwareService { @@ -174,7 +176,9 @@ public List getAllGroupPrivileges(long groupId) { } public void savePrivileges(GroupPrivilegeContractWeb[] requests, Organisation organisation) { - List groupPrivileges = groupPrivilegeRepository.findAll(); + List groupPrivileges = groupPrivilegeRepository.findAll() + .stream().filter(groupPrivilege -> groupPrivilege.getImplVersion() == GroupPrivilege.IMPL_VERSION) + .collect(Collectors.toList()); List privileges = privilegeRepository.findAll(); List subjectTypes = subjectTypeRepository.findAll(); List programs = programRepository.findAll(); @@ -183,21 +187,30 @@ public void savePrivileges(GroupPrivilegeContractWeb[] requests, Organisation or List groups = groupRepository.findAll(); Arrays.stream(requests).forEach(request -> { - GroupPrivilege groupPrivilege = CollectionUtil.findByUuid(groupPrivileges, request.getUuid()); + GroupPrivilege groupPrivilege = groupPrivileges.stream().filter(gp -> + Objects.equals(request.getGroupUUID(), gp.getGroupUuid()) + && Objects.equals(request.getPrivilegeUUID(), gp.getPrivilegeUuid()) + && Objects.equals(request.getSubjectTypeUUID(), gp.getSubjectTypeUuid()) + && Objects.equals(request.getProgramUUID(), gp.getProgramUuid()) + && Objects.equals(request.getProgramEncounterTypeUUID(), gp.getProgramEncounterTypeUuid()) + && Objects.equals(request.getEncounterTypeUUID(), gp.getEncounterTypeUuid()) + && Objects.equals(request.getChecklistDetailUUID(), gp.getChecklistDetailUuid())) + .findAny().orElse(null); if (groupPrivilege == null) { groupPrivilege = new GroupPrivilege(); - groupPrivilege.setUuid(request.getUuid()); + //don't use uuid from request for bundle uploads since there could be records with matching uuid with older impl_version in db and unique org_uuid constraint is violated + groupPrivilege.assignUUID(); + groupPrivilege.setPrivilege(CollectionUtil.findByUuid(privileges, request.getPrivilegeUUID())); + groupPrivilege.setSubjectType(CollectionUtil.findByUuid(subjectTypes, request.getSubjectTypeUUID())); + groupPrivilege.setProgram(CollectionUtil.findByUuid(programs, request.getProgramUUID())); + groupPrivilege.setEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getEncounterTypeUUID())); + groupPrivilege.setProgramEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getProgramEncounterTypeUUID())); + groupPrivilege.setChecklistDetail(CollectionUtil.findByUuid(checklistDetails, request.getChecklistDetailUUID())); + groupPrivilege.setGroup(getGroup(request, organisation, groups)); } - groupPrivilege.setPrivilege(CollectionUtil.findByUuid(privileges, request.getPrivilegeUUID())); - groupPrivilege.setSubjectType(CollectionUtil.findByUuid(subjectTypes, request.getSubjectTypeUUID())); - groupPrivilege.setProgram(CollectionUtil.findByUuid(programs, request.getProgramUUID())); - groupPrivilege.setEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getEncounterTypeUUID())); - groupPrivilege.setProgramEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getProgramEncounterTypeUUID())); - groupPrivilege.setChecklistDetail(CollectionUtil.findByUuid(checklistDetails, request.getChecklistDetailUUID())); - - groupPrivilege.setGroup(getGroup(request, organisation, groups)); + groupPrivilege.setAllow(request.isAllow()); - groupPrivilegeRepository.save(groupPrivilege); + groupPrivilegeRepository.saveGroupPrivilege(groupPrivilege); }); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/GroupPrivilegeController.java b/avni-server-api/src/main/java/org/avni/server/web/GroupPrivilegeController.java index 269b0f299..eff7fcd1c 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/GroupPrivilegeController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/GroupPrivilegeController.java @@ -46,7 +46,7 @@ public GroupPrivilegeController(GroupPrivilegeRepository groupPrivilegeRepositor @RequestMapping(value = "/groups/{id}/privileges", method = RequestMethod.GET) public List getById(@PathVariable("id") Long id) { List allPossibleGroupPrivileges = groupPrivilegeService.getAllGroupPrivileges(id); - List groupPrivileges = groupPrivilegeRepository.findByGroup_Id(id); + List groupPrivileges = groupPrivilegeRepository.findByGroup_IdAndImplVersion(id, GroupPrivilege.IMPL_VERSION); groupPrivileges.addAll(allPossibleGroupPrivileges); return groupPrivileges.stream() .map(GroupPrivilegeContract::fromEntity) @@ -61,6 +61,7 @@ public ResponseEntity addOrUpdateGroupPrivileges(@RequestBody List privilegesToBeAddedOrUpdated = new ArrayList<>(); for (GroupPrivilegeWebRequest groupPrivilegeRequest : request) { + //continue to rely on uuid here since privilege list on webapp will either be new records or valid impl_version existing record for the same org GroupPrivilege groupPrivilege = groupPrivilegeRepository.findByUuid(groupPrivilegeRequest.getUuid()); if (groupPrivilege != null) { groupPrivilege.setAllow(groupPrivilegeRequest.isAllow()); @@ -91,6 +92,6 @@ public ResponseEntity addOrUpdateGroupPrivileges(@RequestBody List groupPrivilegeId + and gp.impl_version = 1 + and implVersion = 1 + ) then + raise 'Duplicate group privilege exists for: id: %, group_id: %, privilege_id: % subject_type_id: %, program_id: %, program_encounter_type_id: %, encounter_type_id: %, checklist_detail_id: %', groupPrivilegeId, groupId, privilegeId, subjectTypeId, programId, programEncounterTypeId, encounterTypeId, checklistDetailId; + end if; + + return true; +end +$$; + +alter table group_privilege + add constraint check_group_privilege_unique + check (check_group_privilege_uniqueness(id, group_id, privilege_id, subject_type_id, program_id, program_encounter_type_id, encounter_type_id, checklist_detail_id, impl_version)); \ No newline at end of file diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java index 56d8548e9..dd47b3590 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java @@ -39,7 +39,7 @@ private void givePrivilege(Group group, GroupPrivilege groupPrivilege, Privilege Privilege p = privilegeRepository.findByType(privilegeType); groupPrivilege.setPrivilege(p); groupPrivilege.setGroup(group); - groupPrivilegeRepository.save(groupPrivilege); + groupPrivilegeRepository.saveGroupPrivilege(groupPrivilege); } public void giveViewSubjectPrivilegeTo(Group group, SubjectType ... subjectTypes) { From d25c4d4accf742220cd287853426b73f7158ec9e Mon Sep 17 00:00:00 2001 From: Joy A Date: Thu, 11 Jul 2024 10:10:33 +0530 Subject: [PATCH 016/129] #721 | Fetch existing group privileges by impl_version during bundle upload --- .../java/org/avni/server/dao/GroupPrivilegeRepository.java | 2 ++ .../server/service/accessControl/GroupPrivilegeService.java | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java index d9e5c9954..efeba4bb2 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java @@ -60,4 +60,6 @@ default List saveAllGroupPrivileges(List groupPr groupPrivileges.forEach(gp -> gp.setImplVersion(GroupPrivilege.IMPL_VERSION)); return this.saveAll(groupPrivileges); } + + List findByImplVersion(int implVersion); } diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java index 8ed5f5ef7..2a98331ef 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java @@ -22,7 +22,6 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; @Service public class GroupPrivilegeService implements NonScopeAwareService { @@ -176,9 +175,7 @@ public List getAllGroupPrivileges(long groupId) { } public void savePrivileges(GroupPrivilegeContractWeb[] requests, Organisation organisation) { - List groupPrivileges = groupPrivilegeRepository.findAll() - .stream().filter(groupPrivilege -> groupPrivilege.getImplVersion() == GroupPrivilege.IMPL_VERSION) - .collect(Collectors.toList()); + List groupPrivileges = groupPrivilegeRepository.findByImplVersion(GroupPrivilege.IMPL_VERSION); List privileges = privilegeRepository.findAll(); List subjectTypes = subjectTypeRepository.findAll(); List programs = programRepository.findAll(); From e42a1ff95b7cc9d486ec9d467888900ecd4f062f Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 11 Jul 2024 11:46:36 +0530 Subject: [PATCH 017/129] #750 - Get region from account being set on admin user. Inject region to the util. --- .../repository/GlificContactRepository.java | 7 +-- .../service/GroupMessagingService.java | 3 +- .../java/org/avni/server/domain/User.java | 1 - .../batch/csv/creator/ObservationCreator.java | 5 +- .../csv/writer/UserAndCatchmentWriter.java | 3 +- .../server/service/AccountAdminService.java | 6 +-- .../service/EnhancedValidationService.java | 7 +-- .../server/service/IndividualService.java | 7 +-- .../org/avni/server/service/UserService.java | 11 +++-- .../org/avni/server/util/PhoneNumberUtil.java | 48 +++++++++---------- .../java/org/avni/server/util/RegionUtil.java | 20 ++++++++ .../org/avni/server/web/UserController.java | 25 +++++++--- .../avni/server/util/PhoneNumberUtilTest.java | 14 +++--- 13 files changed, 92 insertions(+), 65 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/util/RegionUtil.java diff --git a/avni-server-api/src/main/java/org/avni/messaging/repository/GlificContactRepository.java b/avni-server-api/src/main/java/org/avni/messaging/repository/GlificContactRepository.java index 79c511ebb..1bc9f4fcf 100644 --- a/avni-server-api/src/main/java/org/avni/messaging/repository/GlificContactRepository.java +++ b/avni-server-api/src/main/java/org/avni/messaging/repository/GlificContactRepository.java @@ -8,6 +8,7 @@ import org.avni.messaging.external.GlificRestClient; import org.avni.messaging.service.PhoneNumberNotAvailableOrIncorrectException; import org.avni.server.util.PhoneNumberUtil; +import org.avni.server.util.RegionUtil; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; import org.springframework.core.ParameterizedTypeReference; @@ -62,7 +63,7 @@ public GlificContactRepository(GlificRestClient glificRestClient) { } public String getOrCreateContact(String phoneNumber, String fullName) throws PhoneNumberNotAvailableOrIncorrectException, GlificNotConfiguredException { - if (!PhoneNumberUtil.isValidPhoneNumber(phoneNumber)) { + if (!PhoneNumberUtil.isValidPhoneNumber(phoneNumber, RegionUtil.getCurrentUserRegion())) { throw new PhoneNumberNotAvailableOrIncorrectException(); } @@ -73,7 +74,7 @@ public String getOrCreateContact(String phoneNumber, String fullName) throws Pho } private String createContact(String phoneNumber, String fullName) throws GlificNotConfiguredException { - String message = OPTIN_CONTACT_JSON.replace(PHONE_NUMBER, PhoneNumberUtil.getPhoneNumberInGlificFormat(phoneNumber)) + String message = OPTIN_CONTACT_JSON.replace(PHONE_NUMBER, PhoneNumberUtil.getPhoneNumberInGlificFormat(phoneNumber, RegionUtil.getCurrentUserRegion())) .replace(FULL_NAME, fullName); GlificOptinContactResponse glificOptinContactResponse = glificRestClient.callAPI(message, new ParameterizedTypeReference>() { }); @@ -81,7 +82,7 @@ private String createContact(String phoneNumber, String fullName) throws GlificN } private GlificGetContactsResponse getContact(String phoneNumber) throws GlificNotConfiguredException { - String message = GET_CONTACT_JSON.replace(PHONE_NUMBER, PhoneNumberUtil.getPhoneNumberInGlificFormat(phoneNumber)); + String message = GET_CONTACT_JSON.replace(PHONE_NUMBER, PhoneNumberUtil.getPhoneNumberInGlificFormat(phoneNumber, RegionUtil.getCurrentUserRegion())); return glificRestClient.callAPI(message, new ParameterizedTypeReference>() { }); } diff --git a/avni-server-api/src/main/java/org/avni/messaging/service/GroupMessagingService.java b/avni-server-api/src/main/java/org/avni/messaging/service/GroupMessagingService.java index a3602077f..36254d1f5 100644 --- a/avni-server-api/src/main/java/org/avni/messaging/service/GroupMessagingService.java +++ b/avni-server-api/src/main/java/org/avni/messaging/service/GroupMessagingService.java @@ -16,6 +16,7 @@ import org.avni.server.service.UserService; import org.avni.server.util.A; import org.avni.server.util.PhoneNumberUtil; +import org.avni.server.util.RegionUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -144,7 +145,7 @@ private void logAndNotifyError(GlificContactGroupContactsResponse.GlificContactG } private Optional findNameOfTheContact(GlificContactGroupContactsResponse.GlificContactGroupContacts contactGroupContact) { - PhoneNumberUtil.getStandardFormatPhoneNumber(contactGroupContact.getPhone()); + PhoneNumberUtil.getStandardFormatPhoneNumber(contactGroupContact.getPhone(), RegionUtil.getCurrentUserRegion()); Optional name = Stream.>>of( () -> userService.findByPhoneNumber(contactGroupContact.getPhone()).map(User::getName), () -> individualService.findByPhoneNumber(contactGroupContact.getPhone()).map(Individual::getFirstName)) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/User.java b/avni-server-api/src/main/java/org/avni/server/domain/User.java index 073926ef3..bd0c60a5e 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/User.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/User.java @@ -152,7 +152,6 @@ public void setOrganisationId(Long organisationId) { public String getPhoneNumber() { return phoneNumber; } public void setPhoneNumber(String phoneNumber) { - this.phoneNumber = phoneNumber; } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java index da08857f1..894311dc8 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java @@ -19,6 +19,7 @@ import org.avni.server.service.ObservationService; import org.avni.server.service.S3Service; import org.avni.server.util.PhoneNumberUtil; +import org.avni.server.util.RegionUtil; import org.avni.server.util.S; import org.avni.server.web.request.ObservationRequest; import org.slf4j.Logger; @@ -273,11 +274,11 @@ private Object getMediaObservationValue(String answerValue, List errorMs private Map toPhoneNumberFormat(String phoneNumber, List errorMsgs, String conceptName) { Map phoneNumberObs = new HashMap<>(); - if (!PhoneNumberUtil.isValidPhoneNumber(phoneNumber)) { + if (!PhoneNumberUtil.isValidPhoneNumber(phoneNumber, RegionUtil.getCurrentUserRegion())) { errorMsgs.add(format("Invalid %s provided %s. Please provide valid phone number.", conceptName, phoneNumber)); return null; } - phoneNumberObs.put("phoneNumber", PhoneNumberUtil.getNationalPhoneNumber(phoneNumber)); + phoneNumberObs.put("phoneNumber", PhoneNumberUtil.getNationalPhoneNumber(phoneNumber, RegionUtil.getCurrentUserRegion())); phoneNumberObs.put("verified", false); return phoneNumberObs; } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index f90ab9bdc..86c443fdd 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -7,6 +7,7 @@ import org.avni.server.framework.security.UserContextHolder; import org.avni.server.importer.batch.model.Row; import org.avni.server.service.*; +import org.avni.server.util.RegionUtil; import org.avni.server.util.S; import org.avni.server.web.request.syncAttribute.UserSyncSettings; import org.springframework.batch.item.ItemWriter; @@ -94,7 +95,7 @@ private void write(Row row) throws Exception { } User.validateEmail(email); user.setEmail(email); - userService.setPhoneNumber(phoneNumber, user); + userService.setPhoneNumber(phoneNumber, user, RegionUtil.getCurrentUserRegion()); user.setName(nameOfUser); if (!isNewUser) resetSyncService.recordSyncAttributeValueChangeForUser(user, catchment.getId(), syncSettings); user.setCatchment(catchment); diff --git a/avni-server-api/src/main/java/org/avni/server/service/AccountAdminService.java b/avni-server-api/src/main/java/org/avni/server/service/AccountAdminService.java index 4cac34f65..d0502c8bc 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/AccountAdminService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/AccountAdminService.java @@ -12,10 +12,9 @@ @Service public class AccountAdminService { - private final Logger logger; - private AccountAdminRepository accountAdminRepository; - private AccountRepository accountRepository; + private final AccountAdminRepository accountAdminRepository; + private final AccountRepository accountRepository; public AccountAdminService(AccountAdminRepository accountAdminRepository, AccountRepository accountRepository) { this.accountAdminRepository = accountAdminRepository; @@ -36,5 +35,4 @@ public void createAccountAdmins(User user, List accountIds) { accountAdminRepository.save(accountAdmin); }); } - } diff --git a/avni-server-api/src/main/java/org/avni/server/service/EnhancedValidationService.java b/avni-server-api/src/main/java/org/avni/server/service/EnhancedValidationService.java index e50e65b71..99f3ec6dd 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/EnhancedValidationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/EnhancedValidationService.java @@ -18,10 +18,7 @@ import org.avni.server.dao.SubjectTypeRepository; import org.avni.server.domain.*; import org.avni.server.domain.observation.PhoneNumberObservationValue; -import org.avni.server.util.BugsnagReporter; -import org.avni.server.util.DateTimeUtil; -import org.avni.server.util.ObjectMapperSingleton; -import org.avni.server.util.PhoneNumberUtil; +import org.avni.server.util.*; import org.avni.server.web.request.ObservationRequest; import org.avni.server.web.request.rules.RulesContractWrapper.Decision; import org.avni.server.web.validation.ValidationException; @@ -240,7 +237,7 @@ private String validateAnswer(Concept question, FormElement formElement, Object case PhoneNumber: try { PhoneNumberObservationValue phoneNumber = objectMapper.convertValue(value, PhoneNumberObservationValue.class); - if (PhoneNumberUtil.isValidPhoneNumber(phoneNumber.getPhoneNumber())) { + if (PhoneNumberUtil.isValidPhoneNumber(phoneNumber.getPhoneNumber(), RegionUtil.getCurrentUserRegion())) { return formatErrorMessage(question, value); } } catch (ClassCastException | IllegalArgumentException e) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java b/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java index 507b22024..6c13ecc9c 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java @@ -19,10 +19,7 @@ import org.avni.server.domain.observation.PhoneNumberObservationValue; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.accessControl.AccessControlService; -import org.avni.server.util.BadRequestError; -import org.avni.server.util.ObjectMapperSingleton; -import org.avni.server.util.PhoneNumberUtil; -import org.avni.server.util.S; +import org.avni.server.util.*; import org.avni.server.web.request.*; import org.avni.server.web.request.api.RequestUtils; import org.joda.time.DateTime; @@ -459,7 +456,7 @@ public Optional findByPhoneNumber(String phoneNumber) { if (!phoneNumberConcept.isPresent()) { phoneNumberConcept = conceptService.findContactNumberConcept(); } - phoneNumber = PhoneNumberUtil.getNationalPhoneNumber(phoneNumber); + phoneNumber = PhoneNumberUtil.getNationalPhoneNumber(phoneNumber, RegionUtil.getCurrentUserRegion()); return phoneNumberConcept.isPresent() ? individualRepository.findByConceptWithMatchingPattern(phoneNumberConcept.get(), "%" + phoneNumber) : Optional.empty(); diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserService.java b/avni-server-api/src/main/java/org/avni/server/service/UserService.java index 51978d277..4e59957a7 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserService.java @@ -6,6 +6,7 @@ import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.exception.GroupNotFoundException; import org.avni.server.util.PhoneNumberUtil; +import org.avni.server.util.RegionUtil; import org.avni.server.web.validation.ValidationException; import org.bouncycastle.util.Strings; import org.joda.time.DateTime; @@ -127,7 +128,7 @@ public Optional findById(Long id) { } public Optional findByPhoneNumber(String phoneNumber) { - return userRepository.findByPhoneNumber(PhoneNumberUtil.getStandardFormatPhoneNumber(phoneNumber)); + return userRepository.findByPhoneNumber(PhoneNumberUtil.getStandardFormatPhoneNumber(phoneNumber, RegionUtil.getCurrentUserRegion())); } @Transactional @@ -186,10 +187,10 @@ public void ensureSubjectForUser(User user, SubjectType subjectType) { userSubjectRepository.save(userSubject); } - public void setPhoneNumber(String phoneNumber, User user) { - if (!PhoneNumberUtil.isValidPhoneNumber(phoneNumber)) { - throw new ValidationException(PhoneNumberUtil.getInvalidMessage(phoneNumber)); + public void setPhoneNumber(String phoneNumber, User user, String userRegion) { + if (!PhoneNumberUtil.isValidPhoneNumber(phoneNumber, userRegion)) { + throw new ValidationException(PhoneNumberUtil.getInvalidMessage(phoneNumber, userRegion)); } - user.setPhoneNumber(PhoneNumberUtil.getStandardFormatPhoneNumber(phoneNumber)); + user.setPhoneNumber(PhoneNumberUtil.getStandardFormatPhoneNumber(phoneNumber, userRegion)); } } diff --git a/avni-server-api/src/main/java/org/avni/server/util/PhoneNumberUtil.java b/avni-server-api/src/main/java/org/avni/server/util/PhoneNumberUtil.java index af81a9c44..eced87852 100644 --- a/avni-server-api/src/main/java/org/avni/server/util/PhoneNumberUtil.java +++ b/avni-server-api/src/main/java/org/avni/server/util/PhoneNumberUtil.java @@ -5,57 +5,55 @@ import org.avni.server.framework.security.UserContextHolder; public class PhoneNumberUtil { - private static Phonenumber.PhoneNumber parsePhoneNumber(String phoneNumber) throws NumberParseException { - String region = UserContextHolder.getOrganisation().getAccount().getRegion(); + private static Phonenumber.PhoneNumber parsePhoneNumber(String phoneNumber, String region) throws NumberParseException { com.google.i18n.phonenumbers.PhoneNumberUtil instance = com.google.i18n.phonenumbers.PhoneNumberUtil.getInstance(); return instance.parse(phoneNumber, region); } - public static boolean isValidPhoneNumber(String phoneNumber) { + private static Phonenumber.PhoneNumber getPhoneNumber(String phoneNumber, String region) { try { com.google.i18n.phonenumbers.PhoneNumberUtil instance = com.google.i18n.phonenumbers.PhoneNumberUtil.getInstance(); - return instance.isValidNumber(parsePhoneNumber(phoneNumber)); + return instance.parse(phoneNumber, region); } catch (NumberParseException e) { - return false; + throw new RuntimeException(e); } } - public static String getInvalidMessage(String phoneNumber) { - if (isValidPhoneNumber(phoneNumber)) throw new RuntimeException("Phone number is valid"); + private static String getPhoneNumber(String phoneNumber, com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat phoneNumberFormat, String region) { + com.google.i18n.phonenumbers.PhoneNumberUtil instance = com.google.i18n.phonenumbers.PhoneNumberUtil.getInstance(); + return instance.format(getPhoneNumber(phoneNumber, region), phoneNumberFormat); + } + public static boolean isValidPhoneNumber(String phoneNumber, String region) { try { - Phonenumber.PhoneNumber pn = parsePhoneNumber(phoneNumber); - return "Invalid phone number. CountryCode:" + pn.getCountryCode() + ", NationalNumber:" + pn.getNationalNumber(); + com.google.i18n.phonenumbers.PhoneNumberUtil instance = com.google.i18n.phonenumbers.PhoneNumberUtil.getInstance(); + return instance.isValidNumber(parsePhoneNumber(phoneNumber, region)); } catch (NumberParseException e) { - return e.getMessage(); + return false; } } - public static String getStandardFormatPhoneNumber(String phoneNumber) { - return getPhoneNumber(phoneNumber, com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.E164); - } + public static String getInvalidMessage(String phoneNumber, String region) { + if (isValidPhoneNumber(phoneNumber, region)) throw new RuntimeException("Phone number is valid"); - private static Phonenumber.PhoneNumber getPhoneNumber(String phoneNumber) { try { - String region = UserContextHolder.getOrganisation().getAccount().getRegion(); - com.google.i18n.phonenumbers.PhoneNumberUtil instance = com.google.i18n.phonenumbers.PhoneNumberUtil.getInstance(); - return instance.parse(phoneNumber, region); + Phonenumber.PhoneNumber pn = parsePhoneNumber(phoneNumber, region); + return "Invalid phone number. CountryCode:" + pn.getCountryCode() + ", NationalNumber:" + pn.getNationalNumber(); } catch (NumberParseException e) { - throw new RuntimeException(e); + return e.getMessage(); } } - private static String getPhoneNumber(String phoneNumber, com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat phoneNumberFormat) { - com.google.i18n.phonenumbers.PhoneNumberUtil instance = com.google.i18n.phonenumbers.PhoneNumberUtil.getInstance(); - return instance.format(getPhoneNumber(phoneNumber), phoneNumberFormat); + public static String getStandardFormatPhoneNumber(String phoneNumber, String region) { + return getPhoneNumber(phoneNumber, com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.E164, region); } - public static String getPhoneNumberInGlificFormat(String phoneNumber) { - return PhoneNumberUtil.getStandardFormatPhoneNumber(phoneNumber).replace("+", ""); + public static String getPhoneNumberInGlificFormat(String phoneNumber, String region) { + return PhoneNumberUtil.getStandardFormatPhoneNumber(phoneNumber, region).replace("+", ""); } - public static String getNationalPhoneNumber(String phoneNumber) { - Phonenumber.PhoneNumber pn = getPhoneNumber(phoneNumber); + public static String getNationalPhoneNumber(String phoneNumber, String region) { + Phonenumber.PhoneNumber pn = getPhoneNumber(phoneNumber, region); return "" + pn.getNationalNumber(); } } diff --git a/avni-server-api/src/main/java/org/avni/server/util/RegionUtil.java b/avni-server-api/src/main/java/org/avni/server/util/RegionUtil.java new file mode 100644 index 000000000..2c2371532 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/util/RegionUtil.java @@ -0,0 +1,20 @@ +package org.avni.server.util; + +import org.avni.server.domain.Account; +import org.avni.server.domain.Organisation; +import org.avni.server.domain.User; +import org.avni.server.framework.security.UserContextHolder; + +public class RegionUtil { + public static String getCurrentUserRegion() { + Organisation organisation = UserContextHolder.getOrganisation(); + User user = UserContextHolder.getUser(); + if (organisation != null) { + return organisation.getAccount().getRegion(); + } + if (user != null && user.isAdmin()) { + return Account.DEFAULT_REGION; + } + throw new RuntimeException("Could not determine region"); + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserController.java b/avni-server-api/src/main/java/org/avni/server/web/UserController.java index 77587ccca..513c80d92 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserController.java @@ -14,6 +14,7 @@ import org.avni.server.service.*; import org.avni.server.service.accessControl.AccessControlService; import org.avni.server.util.PhoneNumberUtil; +import org.avni.server.util.RegionUtil; import org.avni.server.util.ValidationUtil; import org.avni.server.util.WebResponseUtil; import org.avni.server.web.request.ChangePasswordRequest; @@ -100,7 +101,7 @@ public ResponseEntity createUser(@RequestBody UserContract userContract) { logger.info(String.format("Creating user with username '%s' and UUID '%s'", userContract.getUsername(), user.getUuid())); user.setUsername(userContract.getUsername()); - user = setUserAttributes(user, userContract); + user = setUserAttributes(user, userContract, getRegionForUser(userContract)); User savedUser = userService.save(user); idpServiceFactory.getIdpService().createSuperAdminWithPassword(savedUser, userContract.getPassword()); @@ -141,7 +142,9 @@ public ResponseEntity updateUser(@RequestBody UserContract userContract, @PathVa User currentUser = userService.getCurrentUser(); user.setAuditInfo(currentUser); resetSyncService.recordSyncAttributeValueChangeForUser(user, userContract, UserSyncSettings.fromUserSyncWebJSON(userContract.getSyncSettings(), subjectTypeRepository)); - user = setUserAttributes(user, userContract); + + String region = getRegionForUser(userContract); + user = setUserAttributes(user, userContract, region); idpServiceFactory.getIdpService(user).updateUser(user); userService.save(user); @@ -156,6 +159,16 @@ public ResponseEntity updateUser(@RequestBody UserContract userContract, @PathVa } } + private String getRegionForUser(UserContract userContract) { + String region; + if (userContract.getAccountIds().isEmpty()) { + region = RegionUtil.getCurrentUserRegion(); + } else { + region = accountRepository.findOne(userContract.getAccountIds().get(0)).getRegion(); + } + return region; + } + private Boolean emailIsValid(String email) { return EmailValidator.getInstance().isValid(email); } @@ -168,12 +181,12 @@ private Boolean isNameInvalid(String name) { return ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(name, NAME_INVALID_CHARS_PATTERN); } - private User setUserAttributes(User user, UserContract userContract) { + private User setUserAttributes(User user, UserContract userContract, String userRegion) { if (!emailIsValid(userContract.getEmail())) throw new ValidationException(String.format("Invalid email address %s", userContract.getEmail())); user.setEmail(userContract.getEmail()); - userService.setPhoneNumber(userContract.getPhoneNumber(), user); + userService.setPhoneNumber(userContract.getPhoneNumber(), user, userRegion); if (isUserNameInvalid(userContract.getUsername())) { throw new ValidationException(String.format("Invalid username %s", userContract.getUsername())); @@ -184,7 +197,7 @@ private User setUserAttributes(User user, UserContract userContract) { } user.setName(userContract.getName()); - if(userContract.getCatchmentId()!=null) { + if (userContract.getCatchmentId() != null) { user.setCatchment(catchmentRepository.findOne(userContract.getCatchmentId())); } @@ -363,7 +376,7 @@ private List getOwnedOrganisationIds(User user) { .map(Organisation::getId).collect(Collectors.toList()); } - @GetMapping( "/user/search/findByOrganisation") + @GetMapping("/user/search/findByOrganisation") @ResponseBody public Page getUsersByOrganisation(@RequestParam("organisationId") Long organisationId, Pageable pageable) { accessControlService.checkPrivilege(PrivilegeType.EditUserConfiguration); diff --git a/avni-server-api/src/test/java/org/avni/server/util/PhoneNumberUtilTest.java b/avni-server-api/src/test/java/org/avni/server/util/PhoneNumberUtilTest.java index 4de295a97..084d761c4 100644 --- a/avni-server-api/src/test/java/org/avni/server/util/PhoneNumberUtilTest.java +++ b/avni-server-api/src/test/java/org/avni/server/util/PhoneNumberUtilTest.java @@ -25,20 +25,20 @@ public void setup() { @Test public void checkFormats() { - assertEquals("919455509147", PhoneNumberUtil.getPhoneNumberInGlificFormat("9455509147")); - assertEquals("9455509147", PhoneNumberUtil.getNationalPhoneNumber("9455509147")); - assertEquals("+919455509147", PhoneNumberUtil.getStandardFormatPhoneNumber("9455509147")); + assertEquals("919455509147", PhoneNumberUtil.getPhoneNumberInGlificFormat("9455509147", "IN")); + assertEquals("9455509147", PhoneNumberUtil.getNationalPhoneNumber("9455509147", "IN")); + assertEquals("+919455509147", PhoneNumberUtil.getStandardFormatPhoneNumber("9455509147", "IN")); } @Test public void isValid() { - assertFalse(PhoneNumberUtil.isValidPhoneNumber("+9111111111")); + assertFalse(PhoneNumberUtil.isValidPhoneNumber("+9111111111", "IN")); } @Test public void getInvalidMessage() { - assertEquals("Invalid phone number. CountryCode:91, NationalNumber:11111111", PhoneNumberUtil.getInvalidMessage("+9111111111")); - assertEquals("Invalid phone number. CountryCode:91, NationalNumber:2829", PhoneNumberUtil.getInvalidMessage("+91 2829")); - assertEquals("The string supplied did not seem to be a phone number.", PhoneNumberUtil.getInvalidMessage("")); + assertEquals("Invalid phone number. CountryCode:91, NationalNumber:11111111", PhoneNumberUtil.getInvalidMessage("+9111111111", "IN")); + assertEquals("Invalid phone number. CountryCode:91, NationalNumber:2829", PhoneNumberUtil.getInvalidMessage("+91 2829", "IN")); + assertEquals("The string supplied did not seem to be a phone number.", PhoneNumberUtil.getInvalidMessage("", "IN")); } } From 4606490380bda62f38ac16e9fd220187c1635841 Mon Sep 17 00:00:00 2001 From: Joy A Date: Thu, 11 Jul 2024 13:00:44 +0530 Subject: [PATCH 018/129] #752 | Respect voided in contract for GroupRole, IndividualRelation and Program --- .../src/main/java/org/avni/server/service/GroupRoleService.java | 1 + .../java/org/avni/server/service/IndividualRelationService.java | 1 + .../src/main/java/org/avni/server/service/ProgramService.java | 1 + .../main/java/org/avni/server/web/request/GroupRoleContract.java | 1 + 4 files changed, 4 insertions(+) diff --git a/avni-server-api/src/main/java/org/avni/server/service/GroupRoleService.java b/avni-server-api/src/main/java/org/avni/server/service/GroupRoleService.java index 310c6d965..ef8674f65 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/GroupRoleService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/GroupRoleService.java @@ -36,6 +36,7 @@ public GroupRole saveGroupRole(GroupRoleContract groupRoleRequest, SubjectType g groupRole.setPrimary(groupRoleRequest.getPrimary()); groupRole.setMaximumNumberOfMembers(groupRoleRequest.getMaximumNumberOfMembers()); groupRole.setMinimumNumberOfMembers(groupRoleRequest.getMinimumNumberOfMembers()); + groupRole.setVoided(groupRoleRequest.isVoided()); return groupRoleRepository.save(groupRole); } diff --git a/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationService.java b/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationService.java index be986e165..50529ea92 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationService.java @@ -58,6 +58,7 @@ public void uploadRelation(IndividualRelationContract individualRelationContract individualRelation.setUuid(uuid == null ? UUID.randomUUID().toString() : uuid); } individualRelation.setName(name); //Update name if changed + individualRelation.setVoided(individualRelationContract.isVoided()); IndividualRelation savedRelation = individualRelationRepository.save(individualRelation); saveGenderMappings(individualRelationContract, savedRelation); } diff --git a/avni-server-api/src/main/java/org/avni/server/service/ProgramService.java b/avni-server-api/src/main/java/org/avni/server/service/ProgramService.java index 0216edbdb..a8a7cfb7b 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ProgramService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ProgramService.java @@ -63,6 +63,7 @@ public void updateAndSaveProgram(Program program, ProgramContract programContrac program.setManualEnrolmentEligibilityCheckRule(programContract.getManualEnrolmentEligibilityCheckRule()); program.setManualEnrolmentEligibilityCheckDeclarativeRule(programContract.getManualEnrolmentEligibilityCheckDeclarativeRule()); program.setAllowMultipleEnrolments(programContract.isAllowMultipleEnrolments()); + program.setVoided(programContract.isVoided()); programRepository.save(program); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/GroupRoleContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/GroupRoleContract.java index 17ffbb076..254d6cd30 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/GroupRoleContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/GroupRoleContract.java @@ -24,6 +24,7 @@ public static GroupRoleContract fromEntity(GroupRole groupRole) { groupRoleContract.setSubjectMemberName(groupRole.getMemberSubjectType().getName()); groupRoleContract.setMaximumNumberOfMembers(groupRole.getMaximumNumberOfMembers()); groupRoleContract.setMinimumNumberOfMembers(groupRole.getMinimumNumberOfMembers()); + groupRoleContract.setVoided(groupRole.isVoided()); return groupRoleContract; } From 1c0ab0d36e9897ef4c489fe5dda6a4b7ca2520d4 Mon Sep 17 00:00:00 2001 From: Joy A Date: Thu, 11 Jul 2024 17:05:26 +0530 Subject: [PATCH 019/129] #752 | Respect voided in contract for Catchment --- .../src/main/java/org/avni/server/service/CatchmentService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java b/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java index 9d0589856..699df0aeb 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java @@ -71,6 +71,7 @@ public List saveAllCatchments(CatchmentsContract catchmentsContract, addAddressLevels(catchmentRequest, catchment); removeObsoleteAddressLevelsFromCatchment(catchment, catchmentRequest); catchment.setOrganisationId(organisation.getId()); + catchment.setVoided(catchmentRequest.isVoided()); catchments.add(catchmentRepository.save(catchment)); } From 77116c823385203b952aadbcab4f32f08e08ddb8 Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 12 Jul 2024 17:35:55 +0530 Subject: [PATCH 020/129] avniproject/avni-client#1458 | Log constraint violation specifics during entity upsert --- .../server/dao/CustomJpaRepositoryImpl.java | 17 +++++++++++++++++ ...ntViolationExceptionAcrossOrganisations.java | 9 +++++++++ .../org/avni/server/web/ErrorInterceptors.java | 9 +++++++++ 3 files changed, 35 insertions(+) create mode 100644 avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java diff --git a/avni-server-api/src/main/java/org/avni/server/dao/CustomJpaRepositoryImpl.java b/avni-server-api/src/main/java/org/avni/server/dao/CustomJpaRepositoryImpl.java index fbb2d3a53..4a7e7f613 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/CustomJpaRepositoryImpl.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/CustomJpaRepositoryImpl.java @@ -1,6 +1,8 @@ package org.avni.server.dao; import org.avni.server.domain.CHSEntity; +import org.avni.server.service.exception.ConstraintViolationExceptionAcrossOrganisations; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; @@ -9,9 +11,11 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import javax.persistence.EntityManager; +import javax.persistence.PersistenceException; import javax.persistence.TypedQuery; import java.io.Serializable; import java.util.List; +import java.util.Objects; public class CustomJpaRepositoryImpl extends SimpleJpaRepository implements CustomCHSJpaRepository { @@ -37,4 +41,17 @@ private Slice readSlice(TypedQuery query, Pageable pageable, Specification } return new SliceImpl<>(content, pageable, hasNextPage); } + + + @Override + public S save(S entity) { + try { + return super.save(entity); + } catch (PersistenceException pe) { + if (Objects.isNull(entity.getId()) && pe.getCause() != null && pe.getCause().getClass().equals(ConstraintViolationException.class)) { + throw new ConstraintViolationExceptionAcrossOrganisations(String.format("Entity=> ID: %d, UUID: %s, Type:%s, User:%s, Msg: %s", entity.getId(), entity.getUuid(), entity.getClass().getCanonicalName(), entity.getLastModifiedByName(), pe.getMessage()), (ConstraintViolationException) pe.getCause()); + } + throw pe; + } + } } \ No newline at end of file diff --git a/avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java b/avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java new file mode 100644 index 000000000..544f0ef5e --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java @@ -0,0 +1,9 @@ +package org.avni.server.service.exception; + +import org.hibernate.exception.ConstraintViolationException; + +public class ConstraintViolationExceptionAcrossOrganisations extends ConstraintViolationException { + public ConstraintViolationExceptionAcrossOrganisations(String message, ConstraintViolationException cve) { + super(message, cve.getSQLException(), cve.getSQL(), cve.getConstraintName()); + } +} \ No newline at end of file diff --git a/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java b/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java index 8c4d3b41e..247e29b96 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java @@ -5,6 +5,7 @@ import org.avni.server.domain.accessControl.AvniAccessException; import org.avni.server.domain.accessControl.AvniNoUserSessionException; import org.avni.server.framework.rest.RestControllerErrorResponse; +import org.avni.server.service.exception.ConstraintViolationExceptionAcrossOrganisations; import org.avni.server.util.BadRequestError; import org.avni.server.util.BugsnagReporter; import org.avni.server.web.util.ErrorBodyBuilder; @@ -91,6 +92,14 @@ public ResponseEntity fileUploadSizeLimitExceededError(Exception e) { return ResponseEntity.badRequest().body(String.format("Maximum upload file size exceeded; ensure file size is less than %s.", maxFileSize)); } + @ExceptionHandler(value = {ConstraintViolationExceptionAcrossOrganisations.class}) + public ResponseEntity entityUpsertErrorDueToConstraintViolationAcrossOrganisations(Exception e) { + bugsnagReporter.logAndReportToBugsnag(e); + return ResponseEntity.status(HttpStatus.CONFLICT).body( + String.format("Entity create or update failed due to constraint violation across organisations: %s", + errorBodyBuilder.getErrorMessageBody(e))); + } + @ExceptionHandler(value = {Exception.class}) public ResponseEntity unknownException(Exception e) { if (e instanceof BadRequestError) { From d87e7459ee1cb6d8a49fb165c98e1b10fe967815 Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 12 Jul 2024 17:36:44 +0530 Subject: [PATCH 021/129] avniproject/avni-client#1458 | Include Response status info in AuthFilter closing log --- .../avni/server/framework/security/AuthenticationFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/framework/security/AuthenticationFilter.java b/avni-server-api/src/main/java/org/avni/server/framework/security/AuthenticationFilter.java index 09cfc96ea..1a00dcf83 100644 --- a/avni-server-api/src/main/java/org/avni/server/framework/security/AuthenticationFilter.java +++ b/avni-server-api/src/main/java/org/avni/server/framework/security/AuthenticationFilter.java @@ -72,7 +72,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse long start = System.currentTimeMillis(); chain.doFilter(request, response); long end = System.currentTimeMillis(); - logger.info(String.format("%s %s?%s User: %s Organisation: %s Time: %s ms", method, requestURI, queryString, userContext.getUserName(), userContext.getOrganisationName(), (end - start))); + logger.info(String.format("%s %s?%s Status: %s User: %s Organisation: %s Time: %s ms", method, requestURI, queryString, response.getStatus(), userContext.getUserName(), userContext.getOrganisationName(), (end - start))); } else { String derivedAuthToken = authTokenManager.getDerivedAuthToken(request, queryString); authTokenManager.setAuthCookie(request, response, derivedAuthToken); From 00f325d0d04c6cf008a3271b1505df49d5d6c5b4 Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 12 Jul 2024 18:11:55 +0530 Subject: [PATCH 022/129] Revert "avniproject/avni-client#1458 | Log constraint violation specifics during entity upsert" This reverts commit 77116c823385203b952aadbcab4f32f08e08ddb8. --- .../server/dao/CustomJpaRepositoryImpl.java | 17 ----------------- ...ntViolationExceptionAcrossOrganisations.java | 9 --------- .../org/avni/server/web/ErrorInterceptors.java | 9 --------- 3 files changed, 35 deletions(-) delete mode 100644 avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java diff --git a/avni-server-api/src/main/java/org/avni/server/dao/CustomJpaRepositoryImpl.java b/avni-server-api/src/main/java/org/avni/server/dao/CustomJpaRepositoryImpl.java index 4a7e7f613..fbb2d3a53 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/CustomJpaRepositoryImpl.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/CustomJpaRepositoryImpl.java @@ -1,8 +1,6 @@ package org.avni.server.dao; import org.avni.server.domain.CHSEntity; -import org.avni.server.service.exception.ConstraintViolationExceptionAcrossOrganisations; -import org.hibernate.exception.ConstraintViolationException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; @@ -11,11 +9,9 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import javax.persistence.EntityManager; -import javax.persistence.PersistenceException; import javax.persistence.TypedQuery; import java.io.Serializable; import java.util.List; -import java.util.Objects; public class CustomJpaRepositoryImpl extends SimpleJpaRepository implements CustomCHSJpaRepository { @@ -41,17 +37,4 @@ private Slice readSlice(TypedQuery query, Pageable pageable, Specification } return new SliceImpl<>(content, pageable, hasNextPage); } - - - @Override - public S save(S entity) { - try { - return super.save(entity); - } catch (PersistenceException pe) { - if (Objects.isNull(entity.getId()) && pe.getCause() != null && pe.getCause().getClass().equals(ConstraintViolationException.class)) { - throw new ConstraintViolationExceptionAcrossOrganisations(String.format("Entity=> ID: %d, UUID: %s, Type:%s, User:%s, Msg: %s", entity.getId(), entity.getUuid(), entity.getClass().getCanonicalName(), entity.getLastModifiedByName(), pe.getMessage()), (ConstraintViolationException) pe.getCause()); - } - throw pe; - } - } } \ No newline at end of file diff --git a/avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java b/avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java deleted file mode 100644 index 544f0ef5e..000000000 --- a/avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.avni.server.service.exception; - -import org.hibernate.exception.ConstraintViolationException; - -public class ConstraintViolationExceptionAcrossOrganisations extends ConstraintViolationException { - public ConstraintViolationExceptionAcrossOrganisations(String message, ConstraintViolationException cve) { - super(message, cve.getSQLException(), cve.getSQL(), cve.getConstraintName()); - } -} \ No newline at end of file diff --git a/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java b/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java index 247e29b96..8c4d3b41e 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java @@ -5,7 +5,6 @@ import org.avni.server.domain.accessControl.AvniAccessException; import org.avni.server.domain.accessControl.AvniNoUserSessionException; import org.avni.server.framework.rest.RestControllerErrorResponse; -import org.avni.server.service.exception.ConstraintViolationExceptionAcrossOrganisations; import org.avni.server.util.BadRequestError; import org.avni.server.util.BugsnagReporter; import org.avni.server.web.util.ErrorBodyBuilder; @@ -92,14 +91,6 @@ public ResponseEntity fileUploadSizeLimitExceededError(Exception e) { return ResponseEntity.badRequest().body(String.format("Maximum upload file size exceeded; ensure file size is less than %s.", maxFileSize)); } - @ExceptionHandler(value = {ConstraintViolationExceptionAcrossOrganisations.class}) - public ResponseEntity entityUpsertErrorDueToConstraintViolationAcrossOrganisations(Exception e) { - bugsnagReporter.logAndReportToBugsnag(e); - return ResponseEntity.status(HttpStatus.CONFLICT).body( - String.format("Entity create or update failed due to constraint violation across organisations: %s", - errorBodyBuilder.getErrorMessageBody(e))); - } - @ExceptionHandler(value = {Exception.class}) public ResponseEntity unknownException(Exception e) { if (e instanceof BadRequestError) { From 3d46d82bfe9514659fd7cd1e3db0becdd39b1d76 Mon Sep 17 00:00:00 2001 From: himeshr Date: Tue, 16 Jul 2024 13:09:12 +0530 Subject: [PATCH 023/129] avniproject/avni-client#1458 | Capture errored entity details when transactional data's constraint violation occurs --- .../OperatingIndividualScopeAwareRepository.java | 15 +++++++++++++++ .../org/avni/server/service/ChecklistService.java | 2 +- .../org/avni/server/service/CommentService.java | 6 +++--- .../avni/server/service/CommentThreadService.java | 4 ++-- .../org/avni/server/service/EncounterService.java | 2 +- .../avni/server/service/GroupSubjectService.java | 2 +- .../avni/server/service/IndividualService.java | 4 ++-- .../server/service/ProgramEncounterService.java | 2 +- .../server/service/ProgramEnrolmentService.java | 12 ++++++------ .../server/service/SubjectMigrationService.java | 2 +- ...aintViolationExceptionAcrossOrganisations.java | 9 +++++++++ .../org/avni/server/web/ErrorInterceptors.java | 11 +++++++++++ 12 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java diff --git a/avni-server-api/src/main/java/org/avni/server/dao/OperatingIndividualScopeAwareRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/OperatingIndividualScopeAwareRepository.java index 4d5f88f82..dc76324e9 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/OperatingIndividualScopeAwareRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/OperatingIndividualScopeAwareRepository.java @@ -5,7 +5,10 @@ import org.avni.server.dao.sync.TransactionDataCriteriaBuilderUtil; import org.avni.server.domain.*; import org.avni.server.framework.security.UserContextHolder; +import org.avni.server.service.exception.ConstraintViolationExceptionAcrossOrganisations; import org.avni.server.util.JsonObjectUtil; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.domain.Specification; @@ -16,6 +19,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Objects; @SuppressWarnings("rawtypes") @NoRepositoryBean @@ -176,4 +180,15 @@ default void addPredicate(CriteriaBui predicates.add(cb.equal(from.get("id"), cb.literal(0))); } } + + default S saveEntity(S entity) { + try { + return save(entity); + } catch (DataIntegrityViolationException dive) { + if (Objects.isNull(entity.getId()) && dive.getCause() != null && dive.getCause().getClass().equals(ConstraintViolationException.class)) { + throw new ConstraintViolationExceptionAcrossOrganisations(String.format("Entity=> ID: %d, UUID: %s, Type:%s, User:%s, Msg: %s", entity.getId(), entity.getUuid(), entity.getClass().getCanonicalName(), entity.getLastModifiedByName(), dive.getMessage()), (ConstraintViolationException) dive.getCause()); + } + throw dive; + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/ChecklistService.java b/avni-server-api/src/main/java/org/avni/server/service/ChecklistService.java index 038357ac0..3ae0e8969 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ChecklistService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ChecklistService.java @@ -37,7 +37,7 @@ public ChecklistItem findChecklistItem(String checklistUUID, String checklistIte @Transactional(Transactional.TxType.REQUIRED) public void saveItem(ChecklistItem checklistItem) { - checklistItemRepository.save(checklistItem); + checklistItemRepository.saveEntity(checklistItem); } @Override diff --git a/avni-server-api/src/main/java/org/avni/server/service/CommentService.java b/avni-server-api/src/main/java/org/avni/server/service/CommentService.java index 30e13fd82..1d5a2c9f0 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CommentService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CommentService.java @@ -33,12 +33,12 @@ public CommentService(CommentRepository commentRepository, IndividualRepository public Comment saveComment(CommentContract commentContract) { Comment comment = new Comment(); buildComment(commentContract, comment); - return commentRepository.save(comment); + return commentRepository.saveEntity(comment); } public Comment editComment(CommentContract commentContract, Comment existingComment) { buildComment(commentContract, existingComment); - return commentRepository.save(existingComment); + return commentRepository.saveEntity(existingComment); } private void buildComment(CommentContract commentContract, Comment comment) { @@ -54,7 +54,7 @@ private void buildComment(CommentContract commentContract, Comment comment) { public Comment deleteComment(Comment comment) { comment.setVoided(true); - return commentRepository.save(comment); + return commentRepository.saveEntity(comment); } @Override diff --git a/avni-server-api/src/main/java/org/avni/server/service/CommentThreadService.java b/avni-server-api/src/main/java/org/avni/server/service/CommentThreadService.java index 89a8f13b8..48a70fccb 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CommentThreadService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CommentThreadService.java @@ -44,13 +44,13 @@ public CommentThread createNewThread(CommentThreadContract threadContract) { comments.add(comment); }); commentThread.setComments(comments); - return commentThreadRepository.save(commentThread); + return commentThreadRepository.saveEntity(commentThread); } public CommentThread resolveThread(CommentThread commentThread) { commentThread.setStatus(CommentThread.CommentThreadStatus.Resolved); commentThread.setResolvedDateTime(new DateTime()); - return commentThreadRepository.save(commentThread); + return commentThreadRepository.saveEntity(commentThread); } @Override diff --git a/avni-server-api/src/main/java/org/avni/server/service/EncounterService.java b/avni-server-api/src/main/java/org/avni/server/service/EncounterService.java index 49bfc16ec..3c78c25cc 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/EncounterService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/EncounterService.java @@ -186,7 +186,7 @@ public Encounter save(Encounter encounter) { if (individual.getAddressLevel() != null) { encounter.setAddressId(individual.getAddressLevel().getId()); } - return encounterRepository.save(encounter); + return encounterRepository.saveEntity(encounter); } public Page search(EncounterSearchRequest encounterSearchRequest) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/GroupSubjectService.java b/avni-server-api/src/main/java/org/avni/server/service/GroupSubjectService.java index dc996e70c..bfe785e97 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/GroupSubjectService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/GroupSubjectService.java @@ -40,7 +40,7 @@ public OperatingIndividualScopeAwareRepository repository() { public GroupSubject save(GroupSubject groupSubject) throws ValidationException { this.addSyncAttributes(groupSubject); assignMemberToTheAssigneeOfGroup(groupSubject); - return groupSubjectRepository.save(groupSubject); + return groupSubjectRepository.saveEntity(groupSubject); } private void assignMemberToTheAssigneeOfGroup(GroupSubject groupSubject) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java b/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java index 8eb6eb7ad..d57a9c778 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java @@ -348,7 +348,7 @@ public Individual voidSubject(Individual individual) { assertNoUnVoidedEncounters(individual); assertNoUnVoidedEnrolments(individual); individual.setVoided(true); - return individualRepository.save(individual); + return individualRepository.saveEntity(individual); } private void assertNoUnVoidedEnrolments(Individual individual) { @@ -396,7 +396,7 @@ public Object getObservationValueForUpload(FormElement formElement, String answe @Messageable(EntityType.Subject) public Individual save(Individual individual) { individual.addConceptSyncAttributeValues(individual.getSubjectType(), individual.getObservations()); - return individualRepository.save(individual); + return individualRepository.saveEntity(individual); } public String findPhoneNumber(long subjectId) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/ProgramEncounterService.java b/avni-server-api/src/main/java/org/avni/server/service/ProgramEncounterService.java index 2ff49d783..0db5b5d8c 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ProgramEncounterService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ProgramEncounterService.java @@ -230,7 +230,7 @@ public ProgramEncounter save(ProgramEncounter programEncounter) { if (individual.getAddressLevel() != null) { programEncounter.setAddressId(individual.getAddressLevel().getId()); } - programEncounter = programEncounterRepository.save(programEncounter); + programEncounter = programEncounterRepository.saveEntity(programEncounter); return programEncounter; } diff --git a/avni-server-api/src/main/java/org/avni/server/service/ProgramEnrolmentService.java b/avni-server-api/src/main/java/org/avni/server/service/ProgramEnrolmentService.java index 3674283b4..97ed53b2c 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ProgramEnrolmentService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ProgramEnrolmentService.java @@ -206,7 +206,7 @@ public ProgramEnrolment programEnrolmentSave(ProgramEnrolmentRequest request) { programEnrolment.setIndividual(individual); saveIdentifierAssignments(programEnrolment, request); } - programEnrolment = programEnrolmentRepository.save(programEnrolment); + programEnrolment = programEnrolmentRepository.saveEntity(programEnrolment); if (request.getVisitSchedules() != null && request.getVisitSchedules().size() > 0) { programEncounterService.saveVisitSchedules(request.getUuid(), request.getVisitSchedules(), null); @@ -223,7 +223,7 @@ public ProgramEnrolment programEnrolmentSave(ProgramEnrolmentRequest request) { @Messageable(EntityType.ProgramEnrolment) public ProgramEnrolment save(ProgramEnrolment programEnrolment) { this.addSyncAttributes(programEnrolment); - return programEnrolmentRepository.save(programEnrolment); + return programEnrolmentRepository.saveEntity(programEnrolment); } private void addSyncAttributes(ProgramEnrolment enrolment) { @@ -250,7 +250,7 @@ private Checklist saveChecklist(ChecklistContract checklistContract, String prog Checklist existingChecklist = checklistRepository.findByProgramEnrolmentId(programEnrolment.getId()); if (existingChecklist != null) { existingChecklist.setBaseDate(checklistContract.getBaseDate()); - return checklistRepository.save(existingChecklist); + return checklistRepository.saveEntity(existingChecklist); } Checklist checklist = new Checklist(); checklist.assignUUIDIfRequired(); @@ -259,7 +259,7 @@ private Checklist saveChecklist(ChecklistContract checklistContract, String prog checklist.setBaseDate(checklistContract.getBaseDate()); checklist.setChecklistDetail(checklistDetail); checklist.setProgramEnrolment(programEnrolment); - Checklist savedChecklist = checklistRepository.save(checklist); + Checklist savedChecklist = checklistRepository.saveEntity(checklist); checklistContract.getItems().forEach(item -> { ChecklistItem checklistItem = new ChecklistItem(); checklistItem.assignUUIDIfRequired(); @@ -267,7 +267,7 @@ private Checklist saveChecklist(ChecklistContract checklistContract, String prog ChecklistItemDetail checklistItemDetail = checklistItemDetailRepository.findByUuid(checklistItemDetailUUID); checklistItem.setChecklistItemDetail(checklistItemDetail); checklistItem.setChecklist(savedChecklist); - checklistItemRepository.save(checklistItem); + checklistItemRepository.saveEntity(checklistItem); }); return savedChecklist; } @@ -276,7 +276,7 @@ private Checklist saveChecklist(ChecklistContract checklistContract, String prog public ProgramEnrolment voidEnrolment(ProgramEnrolment programEnrolment) { assertNoUnVoidedProgramEncounters(programEnrolment); programEnrolment.setVoided(true); - return programEnrolmentRepository.save(programEnrolment); + return programEnrolmentRepository.saveEntity(programEnrolment); } private void assertNoUnVoidedProgramEncounters(ProgramEnrolment programEnrolment) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java index 9f23bedc8..510ac7d2f 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java @@ -127,7 +127,7 @@ public void changeSubjectsAddressLevel(List subjects, AddressLevel d subjects.forEach(individual -> { this.markSubjectMigrationIfRequired(individual.getUuid(), null, destAddressLevel, null, individual.getObservations(), true); individual.setAddressLevel(destAddressLevel); - individualRepository.save(individual); + individualRepository.saveEntity(individual); }); } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java b/avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java new file mode 100644 index 000000000..544f0ef5e --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/service/exception/ConstraintViolationExceptionAcrossOrganisations.java @@ -0,0 +1,9 @@ +package org.avni.server.service.exception; + +import org.hibernate.exception.ConstraintViolationException; + +public class ConstraintViolationExceptionAcrossOrganisations extends ConstraintViolationException { + public ConstraintViolationExceptionAcrossOrganisations(String message, ConstraintViolationException cve) { + super(message, cve.getSQLException(), cve.getSQL(), cve.getConstraintName()); + } +} \ No newline at end of file diff --git a/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java b/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java index 8c4d3b41e..344a3e43c 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ErrorInterceptors.java @@ -5,11 +5,14 @@ import org.avni.server.domain.accessControl.AvniAccessException; import org.avni.server.domain.accessControl.AvniNoUserSessionException; import org.avni.server.framework.rest.RestControllerErrorResponse; +import org.avni.server.service.exception.ConstraintViolationExceptionAcrossOrganisations; import org.avni.server.util.BadRequestError; import org.avni.server.util.BugsnagReporter; import org.avni.server.web.util.ErrorBodyBuilder; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -91,6 +94,14 @@ public ResponseEntity fileUploadSizeLimitExceededError(Exception e) { return ResponseEntity.badRequest().body(String.format("Maximum upload file size exceeded; ensure file size is less than %s.", maxFileSize)); } + @ExceptionHandler(value = {DataIntegrityViolationException.class, ConstraintViolationException.class, ConstraintViolationExceptionAcrossOrganisations.class}) + public ResponseEntity entityUpsertErrorDueToDataConstraintViolation(Exception e) { + bugsnagReporter.logAndReportToBugsnag(e); + return ResponseEntity.status(HttpStatus.CONFLICT).body( + String.format("Entity create or update failed due to constraint violation: %s", + errorBodyBuilder.getErrorMessageBody(e))); + } + @ExceptionHandler(value = {Exception.class}) public ResponseEntity unknownException(Exception e) { if (e instanceof BadRequestError) { From f1bbdf556eb3cdc83fb0afb42717d8872eb7458c Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 17 Jul 2024 16:08:35 +0530 Subject: [PATCH 024/129] avniproject/avni-client#1452 - initialize inputs with default value. --- .../org/avni/server/domain/ReportCard.java | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java b/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java index bbd1da858..25b1f9f9e 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java @@ -41,7 +41,7 @@ public class ReportCard extends OrganisationAwareEntity { @Column @Type(type = "jsonObject") - private JsonObject standardReportCardInput; + private JsonObject standardReportCardInput = new JsonObject(new HashMap<>()); public String getName() { return name; @@ -121,7 +121,7 @@ public List getStandardReportCardInputSubjectTypes() { } public void setStandardReportCardInputSubjectTypes(List subjectTypes) { - safeSetterForStandardReportCardInput("subjectTypes", subjectTypes); + standardReportCardInput.with("subjectTypes", subjectTypes); } public List getStandardReportCardInputPrograms() { @@ -129,7 +129,7 @@ public List getStandardReportCardInputPrograms() { } public void setStandardReportCardInputPrograms(List programs) { - safeSetterForStandardReportCardInput("programs", programs); + standardReportCardInput.with("programs", programs); } public List getStandardReportCardInputEncounterTypes() { @@ -137,24 +137,14 @@ public List getStandardReportCardInputEncounterTypes() { } public void setStandardReportCardInputEncounterTypes(List encounterTypes) { - safeSetterForStandardReportCardInput("encounterTypes", encounterTypes); + standardReportCardInput.with("encounterTypes", encounterTypes); } private List safeGetterForStandardReportCardInput(String key) { return standardReportCardInput == null ? new ArrayList() : standardReportCardInput.getList(key); } - private void safeSetterForStandardReportCardInput(String key, List value) { - if (standardReportCardInput == null) { - standardReportCardInput = new JsonObject(new HashMap<>()); - } - standardReportCardInput.with(key, value); - } - private void safeSetterForStandardReportCardInput(String key, String value) { - if (standardReportCardInput == null) { - standardReportCardInput = new JsonObject(new HashMap<>()); - } standardReportCardInput.with(key, value); } From 574fc4a02d5f40d33f0fe606ecadc718e8cc2a31 Mon Sep 17 00:00:00 2001 From: himeshr Date: Thu, 18 Jul 2024 15:31:25 +0530 Subject: [PATCH 025/129] #721 | IsVoided for GroupPrivileges with IMPL_VERSION == 1 is meaningless, return false for them always --- .../avni/server/domain/accessControl/GroupPrivilege.java | 9 +++++++++ .../service/accessControl/GroupPrivilegeService.java | 2 +- .../server/web/request/GroupPrivilegeContractWeb.java | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/GroupPrivilege.java b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/GroupPrivilege.java index b7d8793f2..b2a0e5275 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/GroupPrivilege.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/GroupPrivilege.java @@ -218,4 +218,13 @@ public int getImplVersion() { public void setImplVersion(int implVersion) { this.implVersion = implVersion; } + + /** + * + * @return For GroupPrivileges with IMPL_VERSION == 1 return FALSE, otherwise return actual db isVoided value . + */ + @Override + public boolean isVoided() { + return getImplVersion() == GroupPrivilege.IMPL_VERSION ? false : super.isVoided(); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java index 2a98331ef..f83373ca1 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java @@ -183,7 +183,7 @@ public void savePrivileges(GroupPrivilegeContractWeb[] requests, Organisation or List checklistDetails = checklistDetailRepository.findAll(); List groups = groupRepository.findAll(); - Arrays.stream(requests).forEach(request -> { + Arrays.stream(requests).filter(grpPrivyConWebRequest -> !grpPrivyConWebRequest.isVoided()).forEach(request -> { GroupPrivilege groupPrivilege = groupPrivileges.stream().filter(gp -> Objects.equals(request.getGroupUUID(), gp.getGroupUuid()) && Objects.equals(request.getPrivilegeUUID(), gp.getPrivilegeUuid()) diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java b/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java index 380117906..9bba2d449 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java @@ -25,6 +25,7 @@ public static GroupPrivilegeContractWeb fromEntity(GroupPrivilege groupPrivilege groupPrivilegeContractWeb.setEncounterTypeUUID(groupPrivilege.getEncounterTypeUuid()); groupPrivilegeContractWeb.setChecklistDetailUUID(groupPrivilege.getChecklistDetailUuid()); groupPrivilegeContractWeb.setAllow(groupPrivilege.isAllow()); + groupPrivilegeContractWeb.setVoided(groupPrivilege.isVoided()); groupPrivilegeContractWeb.setNotEveryoneGroup(!groupPrivilege.getGroup().isEveryone()); return groupPrivilegeContractWeb; } From 57f61c42e3f47233e4ece8c18a7398faaa906634 Mon Sep 17 00:00:00 2001 From: himeshr Date: Thu, 18 Jul 2024 15:32:24 +0530 Subject: [PATCH 026/129] #721 | Use targetGroup's uuid after accounting for Everyone Group during groupPriv import --- .../server/service/accessControl/GroupPrivilegeService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java index f83373ca1..36507e1e1 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java @@ -184,8 +184,9 @@ public void savePrivileges(GroupPrivilegeContractWeb[] requests, Organisation or List groups = groupRepository.findAll(); Arrays.stream(requests).filter(grpPrivyConWebRequest -> !grpPrivyConWebRequest.isVoided()).forEach(request -> { + Group targetedGroup = getGroup(request, organisation, groups); GroupPrivilege groupPrivilege = groupPrivileges.stream().filter(gp -> - Objects.equals(request.getGroupUUID(), gp.getGroupUuid()) + Objects.equals(targetedGroup.getUuid(), gp.getGroupUuid()) && Objects.equals(request.getPrivilegeUUID(), gp.getPrivilegeUuid()) && Objects.equals(request.getSubjectTypeUUID(), gp.getSubjectTypeUuid()) && Objects.equals(request.getProgramUUID(), gp.getProgramUuid()) @@ -203,7 +204,7 @@ public void savePrivileges(GroupPrivilegeContractWeb[] requests, Organisation or groupPrivilege.setEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getEncounterTypeUUID())); groupPrivilege.setProgramEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getProgramEncounterTypeUUID())); groupPrivilege.setChecklistDetail(CollectionUtil.findByUuid(checklistDetails, request.getChecklistDetailUUID())); - groupPrivilege.setGroup(getGroup(request, organisation, groups)); + groupPrivilege.setGroup(targetedGroup); } groupPrivilege.setAllow(request.isAllow()); From 2ace26a6d6a7f39fc559b0bdc39f240c16fcc878 Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 19 Jul 2024 12:07:12 +0530 Subject: [PATCH 027/129] #721 | Undo change to filter out voided groupPrivileges --- .../server/service/accessControl/GroupPrivilegeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java index 36507e1e1..105a0edbd 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java @@ -183,7 +183,7 @@ public void savePrivileges(GroupPrivilegeContractWeb[] requests, Organisation or List checklistDetails = checklistDetailRepository.findAll(); List groups = groupRepository.findAll(); - Arrays.stream(requests).filter(grpPrivyConWebRequest -> !grpPrivyConWebRequest.isVoided()).forEach(request -> { + Arrays.stream(requests).forEach(request -> { Group targetedGroup = getGroup(request, organisation, groups); GroupPrivilege groupPrivilege = groupPrivileges.stream().filter(gp -> Objects.equals(targetedGroup.getUuid(), gp.getGroupUuid()) From c7cfe88fdb05dca3c8289780d8270f32a2ba2b8c Mon Sep 17 00:00:00 2001 From: sachinkadam Date: Mon, 22 Jul 2024 22:29:33 +0530 Subject: [PATCH 028/129] avniproject/avni-webapp#858 | Rename the catchment and user group if it is voided to support same name --- .../src/main/java/org/avni/server/web/CatchmentController.java | 1 + .../src/main/java/org/avni/server/web/GroupsController.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/avni-server-api/src/main/java/org/avni/server/web/CatchmentController.java b/avni-server-api/src/main/java/org/avni/server/web/CatchmentController.java index dfd47743b..4055e5fc4 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/CatchmentController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/CatchmentController.java @@ -148,6 +148,7 @@ public ResponseEntity voidCatchment(@PathVariable("id") Long id) { return ResponseEntity.badRequest().body(ReactAdminUtil.generateJsonError(String.format("AddressLevelType with id %d not found", id))); } catchment.setVoided(true); + catchment.setName(ReactAdminUtil.getVoidedName(catchment.getName(),catchment.getId())); catchmentRepository.save(catchment); return new ResponseEntity<>(CatchmentContract.fromEntity(catchment), HttpStatus.OK); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/GroupsController.java b/avni-server-api/src/main/java/org/avni/server/web/GroupsController.java index 3cf83bed5..2d823b0a8 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/GroupsController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/GroupsController.java @@ -9,6 +9,7 @@ import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.GroupsService; import org.avni.server.service.accessControl.AccessControlService; +import org.avni.server.util.ReactAdminUtil; import org.avni.server.web.request.GroupContract; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -124,6 +125,7 @@ public ResponseEntity voidGroup(@PathVariable("id") Long id) { return ResponseEntity.badRequest().body(String.format("Default group %s cannot be deleted", group.getName())); group.setVoided(true); + group.setName((ReactAdminUtil.getVoidedName(group.getName(), group.getId()))); group.updateAudit(); groupRepository.save(group); From fde707dc39ad3b044edafbcd052c0e1d653d5551 Mon Sep 17 00:00:00 2001 From: Joy A Date: Wed, 24 Jul 2024 13:26:13 +0530 Subject: [PATCH 029/129] avniproject/avni-webapp#1288 | Use second pass to process dependents during checklist save --- .../org/avni/server/builder/ChecklistDetailBuilder.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java index 0331a990b..c0d6dc386 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java @@ -1,9 +1,6 @@ package org.avni.server.builder; import org.avni.server.application.Form; - -import java.util.Map; - import org.avni.server.dao.application.FormRepository; import org.avni.server.domain.ChecklistDetail; import org.avni.server.domain.ChecklistItemDetail; @@ -14,6 +11,7 @@ import java.util.HashMap; import java.util.List; +import java.util.Map; public class ChecklistDetailBuilder extends BaseBuilder { @@ -51,7 +49,6 @@ public ChecklistDetailBuilder withItems(List items) .withVoided(item.isVoided()) .withform(form) .withConcept(concept) - .withLeadItem(builtItems.get(item.getDependentOn())) .withScheduleOnExpiryOfDependency(item.getScheduleOnExpiryOfDependency()) .withMinDaysFromStartDate(item.getMinDaysFromStartDate()) .withMinDaysFromDependent(item.getMinDaysFromDependent()) @@ -59,6 +56,10 @@ public ChecklistDetailBuilder withItems(List items) .build(); builtItems.put(builtItemDetail.getUuid(), builtItemDetail); }); + //set dependentOn after all items in request have been processed so order of items in request does not matter for dependents + items.forEach(item -> new ChecklistItemDetailBuilder(this.get(), getExistingChecklistItemDetail(this.get(), item)) + .withLeadItem(builtItems.get(item.getDependentOn())) + .build()); return this; } From 0ecc2748caa93f5b7866ca12007715ce8e809c60 Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 24 Jul 2024 15:07:42 +0530 Subject: [PATCH 030/129] avniproject/avni-webapp#1288 | avoid using ChecklistItemDetailBuilder to set Lead ChecklistItemDetail --- .../java/org/avni/server/builder/ChecklistDetailBuilder.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java index c0d6dc386..09d6b38a8 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java @@ -57,9 +57,7 @@ public ChecklistDetailBuilder withItems(List items) builtItems.put(builtItemDetail.getUuid(), builtItemDetail); }); //set dependentOn after all items in request have been processed so order of items in request does not matter for dependents - items.forEach(item -> new ChecklistItemDetailBuilder(this.get(), getExistingChecklistItemDetail(this.get(), item)) - .withLeadItem(builtItems.get(item.getDependentOn())) - .build()); + items.forEach(item -> getExistingChecklistItemDetail(this.get(), item).setLeadChecklistItemDetail(builtItems.get(item.getDependentOn()))); return this; } From cd79a1f05437968e7e775295a322e605d3ea8f55 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 25 Jul 2024 12:47:37 +0530 Subject: [PATCH 031/129] #751 - support for gps coordinates and location properties --- .../avni/server/builder/LocationBuilder.java | 5 +++-- .../server/exporter/v2/ExportV2Processor.java | 2 +- .../v2/LongitudinalExportV2TaskletImpl.java | 2 +- .../main/java/org/avni/server/geo/Point.java | 7 +++++-- .../batch/csv/creator/ObservationCreator.java | 16 ++++++++-------- .../batch/csv/writer/LocationWriter.java | 2 +- .../server/service/ObservationService.java | 4 +--- .../avni/server/web/LocationController.java | 4 ++-- .../server/web/request/LocationContract.java | 18 ++++++++++++++++++ .../server/service/ObservationServiceTest.java | 4 +--- .../service/builder/TestGroupService.java | 6 ++++++ 11 files changed, 47 insertions(+), 23 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java index efa23b7b6..2ad7cfd19 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java @@ -12,9 +12,8 @@ import java.util.UUID; public class LocationBuilder extends BaseBuilder { - private final AddressLevelType type; - private LocationRepository locationRepository; + private final LocationRepository locationRepository; public LocationBuilder(AddressLevel existingEntity, AddressLevelType type) { super(existingEntity, new AddressLevel()); @@ -28,6 +27,8 @@ public LocationBuilder copy(LocationContract locationRequest) throws BuilderExce get().setType(type); get().setLegacyId(locationRequest.getLegacyId()); get().setVoided(locationRequest.isVoided()); + get().setGpsCoordinates(locationRequest.getGpsCoordinates()); + get().setLocationProperties(locationRequest.getLocationProperties()); withParentLocation(locationRequest); return this; } diff --git a/avni-server-api/src/main/java/org/avni/server/exporter/v2/ExportV2Processor.java b/avni-server-api/src/main/java/org/avni/server/exporter/v2/ExportV2Processor.java index d388c042f..55dfb8502 100644 --- a/avni-server-api/src/main/java/org/avni/server/exporter/v2/ExportV2Processor.java +++ b/avni-server-api/src/main/java/org/avni/server/exporter/v2/ExportV2Processor.java @@ -36,7 +36,7 @@ public void init() { } @Override - public LongitudinalExportItemRow process(Object exportItem) throws Exception { + public LongitudinalExportItemRow process(Object exportItem) { LongitudinalExportItemRow exportItemRow = new LongitudinalExportItemRow(); Individual individual = initIndividual((Individual) exportItem, exportItemRow); initGeneralEncounters(exportItemRow, individual); diff --git a/avni-server-api/src/main/java/org/avni/server/exporter/v2/LongitudinalExportV2TaskletImpl.java b/avni-server-api/src/main/java/org/avni/server/exporter/v2/LongitudinalExportV2TaskletImpl.java index d86e94322..99db8d021 100644 --- a/avni-server-api/src/main/java/org/avni/server/exporter/v2/LongitudinalExportV2TaskletImpl.java +++ b/avni-server-api/src/main/java/org/avni/server/exporter/v2/LongitudinalExportV2TaskletImpl.java @@ -91,7 +91,7 @@ private void createFileWriter(String uuid, ExecutionContext executionContext) { } private void writeToFile(List rows) throws Exception { - if (rows.size() == 0) return; + if (rows.isEmpty()) return; writer.write(rows); } diff --git a/avni-server-api/src/main/java/org/avni/server/geo/Point.java b/avni-server-api/src/main/java/org/avni/server/geo/Point.java index 1e641f47a..ea74f7c3b 100644 --- a/avni-server-api/src/main/java/org/avni/server/geo/Point.java +++ b/avni-server-api/src/main/java/org/avni/server/geo/Point.java @@ -11,9 +11,12 @@ * @version $Id$ */ public class Point implements Serializable, Cloneable { + private double x; + private double y; - private final double x; - private final double y; + // Required for deserialization + private Point() { + } public Point(double x, double y) { this.x = x; diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java index 894311dc8..4fb9eb409 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java @@ -98,10 +98,10 @@ private Concept findConcept(String name, boolean isChildQuestionGroup) { public ObservationCollection getObservations(Row row, Headers headers, - List errorMsgs, FormType formType, ObservationCollection oldObservations) throws Exception { + List errorMsgs, FormType formType, ObservationCollection oldObservations) { ObservationCollection observationCollection = constructObservations(row, headers, errorMsgs, formType, oldObservations); - if (errorMsgs.size() > 0) { - throw new Exception(String.join(", ", errorMsgs)); + if (!errorMsgs.isEmpty()) { + throw new RuntimeException(String.join(", ", errorMsgs)); } return observationCollection; } @@ -131,7 +131,7 @@ private String getRowValue(FormElement formElement, Row row, Integer questionGro return row.get(concept.getName()); } - private ObservationCollection constructObservations(Row row, Headers headers, List errorMsgs, FormType formType, ObservationCollection oldObservations) throws Exception { + private ObservationCollection constructObservations(Row row, Headers headers, List errorMsgs, FormType formType, ObservationCollection oldObservations) { List observationRequests = new ArrayList<>(); for (Concept concept : getConceptHeaders(headers, row.getHeaders())) { FormElement formElement = getFormElementForObservationConcept(concept, formType); @@ -205,10 +205,10 @@ private List createDecisionFormElement(Set concepts) { }).collect(Collectors.toList()); } - private FormElement getFormElementForObservationConcept(Concept concept, FormType formType) throws Exception { + private FormElement getFormElementForObservationConcept(Concept concept, FormType formType) { List
applicableForms = formRepository.findByFormTypeAndIsVoidedFalse(formType); - if (applicableForms.size() == 0) - throw new Exception(String.format("No forms of type %s found", formType)); + if (applicableForms.isEmpty()) + throw new RuntimeException(String.format("No forms of type %s found", formType)); return applicableForms.stream() .map(f -> { @@ -219,7 +219,7 @@ private FormElement getFormElementForObservationConcept(Concept concept, FormTyp .flatMap(List::stream) .filter(fel -> fel.getConcept().equals(concept)) .findFirst() - .orElseThrow(() -> new Exception("No form element linked to concept found")); + .orElseThrow(() -> new RuntimeException("No form element linked to concept found")); } private Object getObservationValue(FormElement formElement, String answerValue, FormType formType, List errorMsgs, Row row, Headers headers, ObservationCollection oldObservations) throws Exception { diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java index 6b15022f8..c0a28a9f8 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java @@ -122,7 +122,7 @@ private void createLocationWriter(Row row, List allErrorMsgs) throws Exc } } - private void updateLocationProperties(Row row, List allErrorMsgs, AddressLevel location) throws Exception { + private void updateLocationProperties(Row row, List allErrorMsgs, AddressLevel location) { location.setGpsCoordinates(locationCreator.getLocation(row, LocationHeaders.gpsCoordinates, allErrorMsgs)); location.setLocationProperties(observationCreator.getObservations(row, headers, allErrorMsgs, FormType.Location, location.getLocationProperties())); locationRepository.save(location); diff --git a/avni-server-api/src/main/java/org/avni/server/service/ObservationService.java b/avni-server-api/src/main/java/org/avni/server/service/ObservationService.java index 0abc4f55a..523d5025b 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ObservationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ObservationService.java @@ -36,16 +36,14 @@ public class ObservationService { private final IndividualRepository individualRepository; private final LocationRepository locationRepository; private final NamedParameterJdbcTemplate jdbcTemplate; - private final FormRepository formRepository; private final EnhancedValidationService enhancedValidationService; @Autowired - public ObservationService(ConceptRepository conceptRepository, IndividualRepository individualRepository, LocationRepository locationRepository, NamedParameterJdbcTemplate jdbcTemplate, FormRepository formRepository, Optional enhancedValidationService) { + public ObservationService(ConceptRepository conceptRepository, IndividualRepository individualRepository, LocationRepository locationRepository, NamedParameterJdbcTemplate jdbcTemplate, Optional enhancedValidationService) { this.conceptRepository = conceptRepository; this.individualRepository = individualRepository; this.locationRepository = locationRepository; this.jdbcTemplate = jdbcTemplate; - this.formRepository = formRepository; this.enhancedValidationService = enhancedValidationService.orElse(null); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java index 6284804aa..b1629ffbb 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java @@ -46,7 +46,7 @@ public class LocationController implements RestControllerResourceProcessor scopeBasedSyncService; private final AccessControlService accessControlService; - private LocationSyncRepository locationSyncRepository; + private final LocationSyncRepository locationSyncRepository; @Autowired public LocationController(LocationRepository locationRepository, UserService userService, LocationService locationService, ScopeBasedSyncService scopeBasedSyncService, AccessControlService accessControlService, LocationSyncRepository locationSyncRepository) { @@ -141,7 +141,7 @@ public ResponseEntity voidLocation(@PathVariable("id") Long id) { if (location == null) return ResponseEntity.badRequest().body(String.format("Location with id '%d' not found", id)); - if (location.getNonVoidedSubLocations().size() > 0) + if (!location.getNonVoidedSubLocations().isEmpty()) return ResponseEntity.badRequest().body(ReactAdminUtil.generateJsonError( String.format("Cannot delete location '%s' until all sub locations are deleted", location.getTitle())) ); diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java index c5e74fe13..c864b7735 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java @@ -1,6 +1,8 @@ package org.avni.server.web.request; import org.avni.server.domain.AddressLevel; +import org.avni.server.domain.ObservationCollection; +import org.avni.server.geo.Point; import org.springframework.util.StringUtils; import java.util.ArrayList; @@ -15,6 +17,8 @@ public class LocationContract extends ReferenceDataContract { private String organisationUUID; private String addressLevelTypeUUID; private String legacyId; + private Point gpsCoordinates; + private ObservationCollection locationProperties = new ObservationCollection(); public LocationContract() { } @@ -86,6 +90,18 @@ public void setLegacyId(String legacyId) { this.legacyId = legacyId; } + public Point getGpsCoordinates() { + return gpsCoordinates; + } + + public void setGpsCoordinates(Point gpsCoordinates) { + this.gpsCoordinates = gpsCoordinates; + } + + public ObservationCollection getLocationProperties() { + return locationProperties; + } + public static LocationContract fromAddressLevel(AddressLevel addressLevel) { LocationContract contract = new LocationContract(); if (addressLevel == null) return contract; @@ -96,6 +112,8 @@ public static LocationContract fromAddressLevel(AddressLevel addressLevel) { contract.setAddressLevelTypeUUID(addressLevel.getType().getUuid()); contract.setVoided(addressLevel.isVoided()); contract.setLegacyId(addressLevel.getLegacyId()); + contract.setGpsCoordinates(addressLevel.getGpsCoordinates()); + contract.locationProperties = addressLevel.getLocationProperties(); AddressLevel parent = addressLevel.getParent(); if (parent != null) { ReferenceDataContract parentContract = new ReferenceDataContract(); diff --git a/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java index c73c2e7bc..84d648c4d 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java @@ -41,12 +41,10 @@ public class ObservationServiceTest { private ObservationService observationService; - private String INDIVIDUAL_UUID = "0a1bf764-4576-4d71-b8ec-25895a113e81"; - @Before public void setup() { initMocks(this); - observationService = new ObservationService(conceptRepository, individualRepository, locationRepository, namedParameterJdbcTemplate, formRepository, Optional.of(enhancedValidationService)); + observationService = new ObservationService(conceptRepository, individualRepository, locationRepository, namedParameterJdbcTemplate, Optional.of(enhancedValidationService)); } @Test diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java index 56d8548e9..7a391f521 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java @@ -48,6 +48,12 @@ public void giveViewSubjectPrivilegeTo(Group group, SubjectType ... subjectTypes }); } + public void giveEditSubjectPrivilegeTo(Group group, SubjectType ... subjectTypes) { + Arrays.stream(subjectTypes).forEach(subjectType -> { + this.givePrivilege(group, new TestGroupPrivilegeBuilder().withDefaultValuesForNewEntity().setSubjectType(subjectType).build(), PrivilegeType.EditSubject); + }); + } + public void giveViewProgramPrivilegeTo(Group group, SubjectType subjectType, Program... programs) { Arrays.stream(programs).forEach(program -> { this.givePrivilege(group, new TestGroupPrivilegeBuilder().withDefaultValuesForNewEntity().setSubjectType(subjectType).setProgram(program).build(), PrivilegeType.ViewEnrolmentDetails); From a1ebd314ee79af9881c5c1f29dd23af3abc2b025 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 29 Jul 2024 10:24:00 +0530 Subject: [PATCH 032/129] #759 - new category and added org status --- .../src/main/java/org/avni/server/domain/Organisation.java | 2 +- .../avni/server/domain/metadata/OrganisationCategory.java | 5 ----- .../server/domain/organisation/OrganisationCategory.java | 5 +++++ .../avni/server/domain/organisation/OrganisationStatus.java | 5 +++++ .../java/org/avni/server/web/ImplementationController.java | 2 +- .../org/avni/server/web/request/OrganisationContract.java | 2 +- .../org/avni/server/web/request/UserInfoClientContract.java | 1 - .../org/avni/server/web/response/UserInfoWebResponse.java | 2 +- .../resources/db/migration/V1_344__OrganisationStatus.sql | 1 + .../avni/server/domain/factory/TestOrganisationBuilder.java | 2 +- .../org/avni/server/domain/framework/PhoneNumberTest.java | 2 +- 11 files changed, 17 insertions(+), 12 deletions(-) delete mode 100644 avni-server-api/src/main/java/org/avni/server/domain/metadata/OrganisationCategory.java create mode 100644 avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java create mode 100644 avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java create mode 100644 avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql diff --git a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java index f4f842392..4f065e812 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java @@ -1,7 +1,7 @@ package org.avni.server.domain; import com.fasterxml.jackson.annotation.JsonIgnore; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; import org.hibernate.annotations.BatchSize; import javax.persistence.*; diff --git a/avni-server-api/src/main/java/org/avni/server/domain/metadata/OrganisationCategory.java b/avni-server-api/src/main/java/org/avni/server/domain/metadata/OrganisationCategory.java deleted file mode 100644 index 26b118f18..000000000 --- a/avni-server-api/src/main/java/org/avni/server/domain/metadata/OrganisationCategory.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.avni.server.domain.metadata; - -public enum OrganisationCategory { - Production, UAT, Prototype, Temporary -} diff --git a/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java new file mode 100644 index 000000000..fbf5f7fab --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java @@ -0,0 +1,5 @@ +package org.avni.server.domain.organisation; + +public enum OrganisationCategory { + Production, UAT, Prototype, Temporary, Trial +} diff --git a/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java new file mode 100644 index 000000000..f4fabec0f --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java @@ -0,0 +1,5 @@ +package org.avni.server.domain.organisation; + +public enum OrganisationStatus { + Archived, Live +} diff --git a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java index 6a259cd80..ba4ff1530 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java @@ -3,7 +3,7 @@ import org.avni.server.domain.Concept; import org.avni.server.domain.Organisation; import org.avni.server.domain.accessControl.PrivilegeType; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.OrganisationService; import org.avni.server.service.accessControl.AccessControlService; diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java index ceeb5dfaf..2de14282a 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java @@ -1,7 +1,7 @@ package org.avni.server.web.request; import org.avni.server.domain.Organisation; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; public class OrganisationContract extends ETLContract { private Long parentOrganisationId; diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/UserInfoClientContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/UserInfoClientContract.java index f40e102df..fcd72349e 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/UserInfoClientContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/UserInfoClientContract.java @@ -1,7 +1,6 @@ package org.avni.server.web.request; import org.avni.server.domain.JsonObject; -import org.avni.server.domain.metadata.OrganisationCategory; import org.springframework.hateoas.core.Relation; import java.util.List; diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/UserInfoWebResponse.java b/avni-server-api/src/main/java/org/avni/server/web/response/UserInfoWebResponse.java index 129165dc7..fb9ecf724 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/UserInfoWebResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/UserInfoWebResponse.java @@ -4,7 +4,7 @@ import org.avni.server.domain.JsonObject; import org.avni.server.domain.Organisation; import org.avni.server.domain.User; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; import org.avni.server.web.request.UserInfoContract; import java.util.List; diff --git a/avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql b/avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql new file mode 100644 index 000000000..223dedd65 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql @@ -0,0 +1 @@ +alter table organisation add column status varchar(255) not null default 'Live'; diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java index 162d8ac59..dd502f067 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java @@ -2,7 +2,7 @@ import org.avni.server.domain.Account; import org.avni.server.domain.Organisation; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; import java.util.UUID; diff --git a/avni-server-api/src/test/java/org/avni/server/domain/framework/PhoneNumberTest.java b/avni-server-api/src/test/java/org/avni/server/domain/framework/PhoneNumberTest.java index f56493e54..41d294096 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/framework/PhoneNumberTest.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/framework/PhoneNumberTest.java @@ -33,7 +33,7 @@ public void checkOutFeatures() throws NumberParseException { assertEquals("+919455509147", instance.format(instance.parse("9455509147", "IN"), PhoneNumberUtil.PhoneNumberFormat.E164)); } - private static boolean isValidNumber(PhoneNumberUtil instance, String number) throws NumberParseException { + private static boolean isValidNumber(PhoneNumberUtil instance, String number) { try { return instance.isValidNumber(instance.parse(number, "IN")); } catch (NumberParseException e) { From e255b19dc3ac8f457268ab42034ae557144e29f7 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 29 Jul 2024 10:28:06 +0530 Subject: [PATCH 033/129] #759 - renamed wrong migration number --- ...344__OrganisationStatus.sql => V1_343__OrganisationStatus.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename avni-server-api/src/main/resources/db/migration/{V1_344__OrganisationStatus.sql => V1_343__OrganisationStatus.sql} (100%) diff --git a/avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql b/avni-server-api/src/main/resources/db/migration/V1_343__OrganisationStatus.sql similarity index 100% rename from avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql rename to avni-server-api/src/main/resources/db/migration/V1_343__OrganisationStatus.sql From ba9f35c985916be70272a7ef3d30c7f72d5ecaba Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 29 Jul 2024 14:12:04 +0530 Subject: [PATCH 034/129] #759 - added organisation status support --- .../java/org/avni/server/domain/Organisation.java | 12 ++++++++++++ .../org/avni/server/web/OrganisationController.java | 5 +++-- .../server/web/request/OrganisationContract.java | 11 +++++++++++ makefiles/externalDB.mk | 8 ++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java index 4f065e812..1adbccff4 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import org.avni.server.domain.organisation.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationStatus; import org.hibernate.annotations.BatchSize; import javax.persistence.*; @@ -28,6 +29,10 @@ public class Organisation extends ETLEntity { @Enumerated(EnumType.STRING) private OrganisationCategory category; + @NotNull + @Enumerated(EnumType.STRING) + private OrganisationStatus status; + public Organisation() { } @@ -89,4 +94,11 @@ public void setCategory(OrganisationCategory organisationCategory) { this.category = organisationCategory; } + public OrganisationStatus getStatus() { + return status; + } + + public void setStatus(OrganisationStatus status) { + this.status = status; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java index ee5c85d55..44543564a 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java @@ -51,6 +51,7 @@ public ResponseEntity save(@RequestBody OrganisationContract request) { org.setDbUser(request.getDbUser()); org.setSchemaName(request.getSchemaName()); org.setCategory(request.getCategory()); + org.setStatus(request.getStatus()); setAttributesOnOrganisation(request, org); setOrgAccountByIdOrDefault(org, request.getAccountId()); @@ -96,7 +97,6 @@ public Organisation updateOrganisation(@PathVariable Long id, @RequestBody Organ return organisationRepository.save(organisation); } - @RequestMapping(value = "/organisation/search/find", method = RequestMethod.GET) @ResponseBody public Page find(@RequestParam(value = "name", required = false) String name, @@ -115,7 +115,7 @@ public Page find(@RequestParam(value = "name", required = } List predicates = new ArrayList<>(); ownedAccounts.forEach(account -> predicates.add(builder.equal(root.get("account"), account))); - Predicate accountPredicate = builder.or(predicates.toArray(new Predicate[predicates.size()])); + Predicate accountPredicate = builder.or(predicates.toArray(new Predicate[0])); return builder.and(accountPredicate, predicate); }, pageable); return organisations.map(OrganisationContract::fromEntity); @@ -137,6 +137,7 @@ private void setAttributesOnOrganisation(@RequestBody OrganisationContract reque organisation.setMediaDirectory(request.getMediaDirectory()); organisation.setVoided(request.isVoided()); organisation.setCategory(request.getCategory()); + organisation.setStatus(request.getStatus()); } private void setOrgAccountByIdOrDefault(Organisation organisation, Long accountId) { diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java index 2de14282a..7726576cd 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java @@ -2,6 +2,7 @@ import org.avni.server.domain.Organisation; import org.avni.server.domain.organisation.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationStatus; public class OrganisationContract extends ETLContract { private Long parentOrganisationId; @@ -9,6 +10,7 @@ public class OrganisationContract extends ETLContract { private String usernameSuffix; private Long accountId; private OrganisationCategory category; + private OrganisationStatus status; private String region; public static OrganisationContract fromEntity(Organisation organisation) { @@ -20,6 +22,7 @@ public static OrganisationContract fromEntity(Organisation organisation) { organisationContract.setUsernameSuffix(organisation.getEffectiveUsernameSuffix()); organisationContract.setAccountId(organisation.getAccount() == null ? null : organisation.getAccount().getId()); organisationContract.setCategory(organisation.getCategory()); + organisationContract.setStatus(organisation.getStatus()); organisationContract.region = organisation.getAccount().getRegion(); mapEntity(organisationContract, organisation); return organisationContract; @@ -72,4 +75,12 @@ public String getRegion() { public void setRegion(String region) { this.region = region; } + + public OrganisationStatus getStatus() { + return status; + } + + public void setStatus(OrganisationStatus status) { + this.status = status; + } } diff --git a/makefiles/externalDB.mk b/makefiles/externalDB.mk index 433d13b6b..1eacc8dcb 100644 --- a/makefiles/externalDB.mk +++ b/makefiles/externalDB.mk @@ -204,3 +204,11 @@ else -psql -p $(dbPort) -U ${su} -d avni_metabase -c 'create extension if not exists "hstore"'; psql -U reporting_user -d avni_metabase < $(dumpFile) endif + +backup-org-db: +ifndef orgName + @echo "Provde the orgName variable" + exit 1 +else + sudo -u $(su) pg_dump avni_org > ../avni-db-dumps/local-$(orgName).sql +endif From 708399fe3400832730bbc2352ea91fdec06ada7f Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 29 Jul 2024 15:36:34 +0530 Subject: [PATCH 035/129] #768 - cannot overload methods in rest repo. test to check if there are duplicate methods. --- .../server/dao/GroupPrivilegeRepository.java | 1 - .../org/avni/server/dao/GroupRepository.java | 1 - .../dao/StandardReportCardTypeRepository.java | 1 - .../org/avni/server/dao/UserRepository.java | 5 +- .../application/FormMappingRepository.java | 2 - .../org/avni/server/service/UserService.java | 2 +- .../avni/server/dao/UserRepositoryTest.java | 4 +- .../framework/jpa/AvniRestRepositoryTest.java | 88 +++++++++++++++++++ 8 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 avni-server-api/src/test/java/org/avni/server/framework/jpa/AvniRestRepositoryTest.java diff --git a/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java index efeba4bb2..a10972cca 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/GroupPrivilegeRepository.java @@ -16,7 +16,6 @@ @Repository @RepositoryRestResource(collectionResourceRel = "groupPrivilege", path = "groupPrivilege") - public interface GroupPrivilegeRepository extends ReferenceDataRepository { default GroupPrivilege findByName(String name) { throw new UnsupportedOperationException("No field 'name' in GroupPrivilege."); diff --git a/avni-server-api/src/main/java/org/avni/server/dao/GroupRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/GroupRepository.java index 5af71b8c1..51b36556b 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/GroupRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/GroupRepository.java @@ -11,7 +11,6 @@ @Repository @RepositoryRestResource(collectionResourceRel = "groups", path = "groups") - public interface GroupRepository extends ReferenceDataRepository, FindByLastModifiedDateTime { Group findByNameAndOrganisationId(String name, Long organisationId); diff --git a/avni-server-api/src/main/java/org/avni/server/dao/StandardReportCardTypeRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/StandardReportCardTypeRepository.java index 7c5afffc9..979f9297e 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/StandardReportCardTypeRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/StandardReportCardTypeRepository.java @@ -14,7 +14,6 @@ @Repository @RepositoryRestResource(collectionResourceRel = "standardReportCardType", path = "standardReportCardType") - public interface StandardReportCardTypeRepository extends AvniJpaRepository { StandardReportCardType findByUuid(String uuid); diff --git a/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java index b826400ac..d232a0a7b 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java @@ -2,7 +2,6 @@ import org.avni.server.domain.Catchment; import org.avni.server.domain.User; -import org.avni.server.domain.accessControl.PrivilegeType; import org.avni.server.projection.UserWebProjection; import org.avni.server.web.request.api.RequestUtils; import org.springframework.data.domain.Page; @@ -11,11 +10,9 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryHints; -import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Repository; import org.joda.time.DateTime; @@ -142,5 +139,5 @@ default User getDefaultSuperAdmin() { List getUsersWithSameIdPrefix(String prefix, long exceptUserId); @Query(value = "select * from users where lower(users.settings->>'idPrefix') = lower(:prefix)", nativeQuery = true) - List getUsersWithSameIdPrefix(String prefix); + List getAllUsersWithSameIdPrefix(String prefix); } diff --git a/avni-server-api/src/main/java/org/avni/server/dao/application/FormMappingRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/application/FormMappingRepository.java index f35972727..91ca9f2d5 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/application/FormMappingRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/application/FormMappingRepository.java @@ -8,12 +8,10 @@ import org.avni.server.domain.EncounterType; import org.avni.server.domain.Program; import org.avni.server.domain.SubjectType; -import org.avni.server.domain.ValidationException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.rest.core.annotation.RepositoryRestResource; -import org.springframework.orm.jpa.JpaSystemException; import org.springframework.stereotype.Repository; import java.util.List; diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserService.java b/avni-server-api/src/main/java/org/avni/server/service/UserService.java index f9d13ee78..91345d47d 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserService.java @@ -52,7 +52,7 @@ public User save(User user) { String idPrefix = UserSettings.getIdPrefix(user.getSettings()); if (StringUtils.hasLength(idPrefix)) { synchronized (String.format("%d-USER-ID-PREFIX-%s", user.getOrganisationId(), idPrefix).intern()) { - List usersWithSameIdPrefix = user.isNew() ? userRepository.getUsersWithSameIdPrefix(idPrefix) : userRepository.getUsersWithSameIdPrefix(idPrefix, user.getId()); + List usersWithSameIdPrefix = user.isNew() ? userRepository.getAllUsersWithSameIdPrefix(idPrefix) : userRepository.getUsersWithSameIdPrefix(idPrefix, user.getId()); if (usersWithSameIdPrefix.isEmpty()) { return createUpdateUser(user); } else { diff --git a/avni-server-api/src/test/java/org/avni/server/dao/UserRepositoryTest.java b/avni-server-api/src/test/java/org/avni/server/dao/UserRepositoryTest.java index 56ee54912..3bfaca558 100644 --- a/avni-server-api/src/test/java/org/avni/server/dao/UserRepositoryTest.java +++ b/avni-server-api/src/test/java/org/avni/server/dao/UserRepositoryTest.java @@ -3,12 +3,12 @@ import org.avni.server.common.AbstractControllerIntegrationTest; import org.avni.server.domain.accessControl.PrivilegeType; import org.avni.server.framework.security.UserContextHolder; -import org.flywaydb.core.api.android.ContextHolder; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; @Sql({"/test-data.sql"}) public class UserRepositoryTest extends AbstractControllerIntegrationTest { diff --git a/avni-server-api/src/test/java/org/avni/server/framework/jpa/AvniRestRepositoryTest.java b/avni-server-api/src/test/java/org/avni/server/framework/jpa/AvniRestRepositoryTest.java new file mode 100644 index 000000000..e3812d6c6 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/framework/jpa/AvniRestRepositoryTest.java @@ -0,0 +1,88 @@ +package org.avni.server.framework.jpa; + +import org.avni.server.dao.*; +import org.avni.server.dao.application.*; +import org.avni.server.dao.individualRelationship.IndividualRelationGenderMappingRepository; +import org.avni.server.dao.individualRelationship.IndividualRelationRepository; +import org.avni.server.dao.individualRelationship.IndividualRelationshipTypeRepository; +import org.avni.server.dao.task.TaskStatusRepository; +import org.avni.server.dao.task.TaskTypeRepository; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +public class AvniRestRepositoryTest { + @Test + public void cannotHaveOverloadedMethodsIfRestRepositoryIsExported() { + assertNoDuplicates(UserRepository.class); + assertNoDuplicates(FormElementGroupRepository.class); + assertNoDuplicates(FormElementRepository.class); + assertNoDuplicates(FormMappingRepository.class); + assertNoDuplicates(FormRepository.class); + assertNoDuplicates(MenuItemRepository.class); + assertNoDuplicates(IndividualRelationGenderMappingRepository.class); + assertNoDuplicates(IndividualRelationRepository.class); + assertNoDuplicates(IndividualRelationshipTypeRepository.class); + assertNoDuplicates(TaskStatusRepository.class); + assertNoDuplicates(TaskTypeRepository.class); + assertNoDuplicates(AccountAdminRepository.class); + assertNoDuplicates(AccountRepository.class); + assertNoDuplicates(AddressLevelTypeRepository.class); + assertNoDuplicates(ApprovalStatusRepository.class); + assertNoDuplicates(CatchmentRepository.class); + assertNoDuplicates(ChecklistDetailRepository.class); + assertNoDuplicates(ChecklistItemDetailRepository.class); + assertNoDuplicates(ConceptAnswerRepository.class); + assertNoDuplicates(ConceptRepository.class); + assertNoDuplicates(DashboardFilterRepository.class); + assertNoDuplicates(DashboardRepository.class); + assertNoDuplicates(DashboardSectionCardMappingRepository.class); + assertNoDuplicates(DashboardSectionRepository.class); + assertNoDuplicates(EncounterTypeRepository.class); + assertNoDuplicates(EntityApprovalStatusRepository.class); + assertNoDuplicates(GenderRepository.class); + assertNoDuplicates(GroupDashboardRepository.class); + assertNoDuplicates(GroupPrivilegeRepository.class); + assertNoDuplicates(GroupRepository.class); + assertNoDuplicates(GroupRoleRepository.class); + assertNoDuplicates(IdentifierAssignmentRepository.class); + assertNoDuplicates(IdentifierUserAssignmentRepository.class); + assertNoDuplicates(LocationRepository.class); + assertNoDuplicates(NewsRepository.class); + assertNoDuplicates(OperationalEncounterTypeRepository.class); + assertNoDuplicates(OperationalProgramRepository.class); + assertNoDuplicates(OperationalSubjectTypeRepository.class); + assertNoDuplicates(OrganisationConfigRepository.class); + assertNoDuplicates(OrganisationGroupRepository.class); + assertNoDuplicates(OrganisationRepository.class); + assertNoDuplicates(PrivilegeRepository.class); + assertNoDuplicates(ProgramRepository.class); + assertNoDuplicates(RuleDependencyRepository.class); + assertNoDuplicates(RuleRepository.class); + assertNoDuplicates(StandardReportCardTypeRepository.class); + assertNoDuplicates(SubjectTypeRepository.class); + assertNoDuplicates(TranslationRepository.class); + assertNoDuplicates(UserGroupRepository.class); + assertNoDuplicates(VideoRepository.class); + assertNoDuplicates(VideoTelemetricRepository.class); + } + + private static void assertNoDuplicates(Class clas) { + List duplicates = Arrays.stream(clas.getMethods()) + .filter(method -> !method.isDefault() && method.getDeclaringClass().equals(clas)) + .collect(Collectors.groupingBy(Method::getName)) + .entrySet() + .stream() + .filter(p -> p.getValue().size() > 1) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + duplicates.size(); + assertEquals(0, duplicates.size()); + } +} From 0bb13325278c6c678fe4114ddfb75849178a664b Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 29 Jul 2024 10:24:00 +0530 Subject: [PATCH 036/129] #759 - new category and added org status --- .../src/main/java/org/avni/server/domain/Organisation.java | 2 +- .../avni/server/domain/metadata/OrganisationCategory.java | 5 ----- .../server/domain/organisation/OrganisationCategory.java | 5 +++++ .../avni/server/domain/organisation/OrganisationStatus.java | 5 +++++ .../java/org/avni/server/web/ImplementationController.java | 2 +- .../org/avni/server/web/request/OrganisationContract.java | 2 +- .../org/avni/server/web/request/UserInfoClientContract.java | 1 - .../org/avni/server/web/response/UserInfoWebResponse.java | 2 +- .../resources/db/migration/V1_344__OrganisationStatus.sql | 1 + .../avni/server/domain/factory/TestOrganisationBuilder.java | 2 +- 10 files changed, 16 insertions(+), 11 deletions(-) delete mode 100644 avni-server-api/src/main/java/org/avni/server/domain/metadata/OrganisationCategory.java create mode 100644 avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java create mode 100644 avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java create mode 100644 avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql diff --git a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java index d619ec348..e146a90ac 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java @@ -1,7 +1,7 @@ package org.avni.server.domain; import com.fasterxml.jackson.annotation.JsonIgnore; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; import org.hibernate.annotations.BatchSize; import javax.persistence.*; diff --git a/avni-server-api/src/main/java/org/avni/server/domain/metadata/OrganisationCategory.java b/avni-server-api/src/main/java/org/avni/server/domain/metadata/OrganisationCategory.java deleted file mode 100644 index 26b118f18..000000000 --- a/avni-server-api/src/main/java/org/avni/server/domain/metadata/OrganisationCategory.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.avni.server.domain.metadata; - -public enum OrganisationCategory { - Production, UAT, Prototype, Temporary -} diff --git a/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java new file mode 100644 index 000000000..fbf5f7fab --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java @@ -0,0 +1,5 @@ +package org.avni.server.domain.organisation; + +public enum OrganisationCategory { + Production, UAT, Prototype, Temporary, Trial +} diff --git a/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java new file mode 100644 index 000000000..f4fabec0f --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java @@ -0,0 +1,5 @@ +package org.avni.server.domain.organisation; + +public enum OrganisationStatus { + Archived, Live +} diff --git a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java index 6a259cd80..ba4ff1530 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java @@ -3,7 +3,7 @@ import org.avni.server.domain.Concept; import org.avni.server.domain.Organisation; import org.avni.server.domain.accessControl.PrivilegeType; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.OrganisationService; import org.avni.server.service.accessControl.AccessControlService; diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java index 04a725620..dcad5be79 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java @@ -1,7 +1,7 @@ package org.avni.server.web.request; import org.avni.server.domain.Organisation; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; public class OrganisationContract extends ETLContract { private Long parentOrganisationId; diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/UserInfoClientContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/UserInfoClientContract.java index f40e102df..fcd72349e 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/UserInfoClientContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/UserInfoClientContract.java @@ -1,7 +1,6 @@ package org.avni.server.web.request; import org.avni.server.domain.JsonObject; -import org.avni.server.domain.metadata.OrganisationCategory; import org.springframework.hateoas.core.Relation; import java.util.List; diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/UserInfoWebResponse.java b/avni-server-api/src/main/java/org/avni/server/web/response/UserInfoWebResponse.java index b6ef1e244..841c26e54 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/UserInfoWebResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/UserInfoWebResponse.java @@ -3,7 +3,7 @@ import org.avni.server.domain.JsonObject; import org.avni.server.domain.Organisation; import org.avni.server.domain.User; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; import org.avni.server.web.request.UserInfoContract; import java.util.List; diff --git a/avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql b/avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql new file mode 100644 index 000000000..223dedd65 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql @@ -0,0 +1 @@ +alter table organisation add column status varchar(255) not null default 'Live'; diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java index 162d8ac59..dd502f067 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java @@ -2,7 +2,7 @@ import org.avni.server.domain.Account; import org.avni.server.domain.Organisation; -import org.avni.server.domain.metadata.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationCategory; import java.util.UUID; From e33da149ff7274b280e921a8da40c47ced66204e Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 29 Jul 2024 10:28:06 +0530 Subject: [PATCH 037/129] #759 - renamed wrong migration number --- ...344__OrganisationStatus.sql => V1_343__OrganisationStatus.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename avni-server-api/src/main/resources/db/migration/{V1_344__OrganisationStatus.sql => V1_343__OrganisationStatus.sql} (100%) diff --git a/avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql b/avni-server-api/src/main/resources/db/migration/V1_343__OrganisationStatus.sql similarity index 100% rename from avni-server-api/src/main/resources/db/migration/V1_344__OrganisationStatus.sql rename to avni-server-api/src/main/resources/db/migration/V1_343__OrganisationStatus.sql From abf31edc5f781bcb054f793d26a9476b6a6015ba Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 29 Jul 2024 14:12:04 +0530 Subject: [PATCH 038/129] #759 - added organisation status support --- .../java/org/avni/server/domain/Organisation.java | 13 +++++++++++++ .../org/avni/server/web/OrganisationController.java | 5 +++-- .../server/web/request/OrganisationContract.java | 11 +++++++++++ makefiles/externalDB.mk | 8 ++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java index e146a90ac..7acec03b5 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import org.avni.server.domain.organisation.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationStatus; import org.hibernate.annotations.BatchSize; import javax.persistence.*; @@ -28,6 +29,10 @@ public class Organisation extends ETLEntity { @Enumerated(EnumType.STRING) private OrganisationCategory category; + @NotNull + @Enumerated(EnumType.STRING) + private OrganisationStatus status; + public Organisation() { } @@ -88,4 +93,12 @@ public OrganisationCategory getCategory() { public void setCategory(OrganisationCategory organisationCategory) { this.category = organisationCategory; } + + public OrganisationStatus getStatus() { + return status; + } + + public void setStatus(OrganisationStatus status) { + this.status = status; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java index ee5c85d55..44543564a 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java @@ -51,6 +51,7 @@ public ResponseEntity save(@RequestBody OrganisationContract request) { org.setDbUser(request.getDbUser()); org.setSchemaName(request.getSchemaName()); org.setCategory(request.getCategory()); + org.setStatus(request.getStatus()); setAttributesOnOrganisation(request, org); setOrgAccountByIdOrDefault(org, request.getAccountId()); @@ -96,7 +97,6 @@ public Organisation updateOrganisation(@PathVariable Long id, @RequestBody Organ return organisationRepository.save(organisation); } - @RequestMapping(value = "/organisation/search/find", method = RequestMethod.GET) @ResponseBody public Page find(@RequestParam(value = "name", required = false) String name, @@ -115,7 +115,7 @@ public Page find(@RequestParam(value = "name", required = } List predicates = new ArrayList<>(); ownedAccounts.forEach(account -> predicates.add(builder.equal(root.get("account"), account))); - Predicate accountPredicate = builder.or(predicates.toArray(new Predicate[predicates.size()])); + Predicate accountPredicate = builder.or(predicates.toArray(new Predicate[0])); return builder.and(accountPredicate, predicate); }, pageable); return organisations.map(OrganisationContract::fromEntity); @@ -137,6 +137,7 @@ private void setAttributesOnOrganisation(@RequestBody OrganisationContract reque organisation.setMediaDirectory(request.getMediaDirectory()); organisation.setVoided(request.isVoided()); organisation.setCategory(request.getCategory()); + organisation.setStatus(request.getStatus()); } private void setOrgAccountByIdOrDefault(Organisation organisation, Long accountId) { diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java index dcad5be79..e4096698c 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/OrganisationContract.java @@ -2,6 +2,7 @@ import org.avni.server.domain.Organisation; import org.avni.server.domain.organisation.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationStatus; public class OrganisationContract extends ETLContract { private Long parentOrganisationId; @@ -9,6 +10,7 @@ public class OrganisationContract extends ETLContract { private String usernameSuffix; private Long accountId; private OrganisationCategory category; + private OrganisationStatus status; public static OrganisationContract fromEntity(Organisation organisation) { OrganisationContract organisationContract = new OrganisationContract(); @@ -19,6 +21,7 @@ public static OrganisationContract fromEntity(Organisation organisation) { organisationContract.setUsernameSuffix(organisation.getEffectiveUsernameSuffix()); organisationContract.setAccountId(organisation.getAccount() == null ? null : organisation.getAccount().getId()); organisationContract.setCategory(organisation.getCategory()); + organisationContract.setStatus(organisation.getStatus()); mapEntity(organisationContract, organisation); return organisationContract; } @@ -62,4 +65,12 @@ public OrganisationCategory getCategory() { public void setCategory(OrganisationCategory category) { this.category = category; } + + public OrganisationStatus getStatus() { + return status; + } + + public void setStatus(OrganisationStatus status) { + this.status = status; + } } diff --git a/makefiles/externalDB.mk b/makefiles/externalDB.mk index 433d13b6b..1eacc8dcb 100644 --- a/makefiles/externalDB.mk +++ b/makefiles/externalDB.mk @@ -204,3 +204,11 @@ else -psql -p $(dbPort) -U ${su} -d avni_metabase -c 'create extension if not exists "hstore"'; psql -U reporting_user -d avni_metabase < $(dumpFile) endif + +backup-org-db: +ifndef orgName + @echo "Provde the orgName variable" + exit 1 +else + sudo -u $(su) pg_dump avni_org > ../avni-db-dumps/local-$(orgName).sql +endif From 7a5ce7215427d6644a7b5f48ba355ff4adeb53b1 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 30 Jul 2024 13:58:57 +0530 Subject: [PATCH 039/129] #759 - set org status --- .../server/domain/factory/TestOrganisationBuilder.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java index dd502f067..3071c7b5f 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java @@ -3,6 +3,7 @@ import org.avni.server.domain.Account; import org.avni.server.domain.Organisation; import org.avni.server.domain.organisation.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationStatus; import java.util.UUID; @@ -11,7 +12,7 @@ public class TestOrganisationBuilder { public TestOrganisationBuilder withMandatoryFields() { String placeholder = UUID.randomUUID().toString(); - return withUuid(placeholder).withDbUser("testDbUser").withName(placeholder).withSchemaName(placeholder).setCategory(OrganisationCategory.Production); + return withUuid(placeholder).withDbUser("testDbUser").withName(placeholder).withSchemaName(placeholder).setCategory(OrganisationCategory.Production).withStatus(OrganisationStatus.Live); } public TestOrganisationBuilder setId(long id) { @@ -49,6 +50,11 @@ public TestOrganisationBuilder setCategory(OrganisationCategory organisationCate return this; } + public TestOrganisationBuilder withStatus(OrganisationStatus status) { + organisation.setStatus(status); + return this; + } + public Organisation build() { return organisation; } From c4e89e139e0017d86dd6d72991499b603e5a6f99 Mon Sep 17 00:00:00 2001 From: himeshr Date: Tue, 30 Jul 2024 14:35:15 +0530 Subject: [PATCH 040/129] #759 | change version to resolve flyway migration issues --- ...3__OrganisationStatus.sql => V1_339_2__OrganisationStatus.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename avni-server-api/src/main/resources/db/migration/{V1_343__OrganisationStatus.sql => V1_339_2__OrganisationStatus.sql} (100%) diff --git a/avni-server-api/src/main/resources/db/migration/V1_343__OrganisationStatus.sql b/avni-server-api/src/main/resources/db/migration/V1_339_2__OrganisationStatus.sql similarity index 100% rename from avni-server-api/src/main/resources/db/migration/V1_343__OrganisationStatus.sql rename to avni-server-api/src/main/resources/db/migration/V1_339_2__OrganisationStatus.sql From ceb7b00e7afa917c4d8194fc90df4376a8261183 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 25 Jul 2024 12:47:37 +0530 Subject: [PATCH 041/129] #751 - support for gps coordinates and location properties --- .../avni/server/builder/LocationBuilder.java | 5 +++-- .../server/exporter/v2/ExportV2Processor.java | 2 +- .../v2/LongitudinalExportV2TaskletImpl.java | 2 +- .../main/java/org/avni/server/geo/Point.java | 7 +++++-- .../batch/csv/creator/ObservationCreator.java | 16 ++++++++-------- .../batch/csv/writer/LocationWriter.java | 8 ++++---- .../server/service/ObservationService.java | 4 +--- .../avni/server/web/LocationController.java | 4 ++-- .../server/web/request/LocationContract.java | 18 ++++++++++++++++++ .../server/service/ObservationServiceTest.java | 4 +--- .../service/builder/TestGroupService.java | 6 ++++++ 11 files changed, 50 insertions(+), 26 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java index efa23b7b6..2ad7cfd19 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java @@ -12,9 +12,8 @@ import java.util.UUID; public class LocationBuilder extends BaseBuilder { - private final AddressLevelType type; - private LocationRepository locationRepository; + private final LocationRepository locationRepository; public LocationBuilder(AddressLevel existingEntity, AddressLevelType type) { super(existingEntity, new AddressLevel()); @@ -28,6 +27,8 @@ public LocationBuilder copy(LocationContract locationRequest) throws BuilderExce get().setType(type); get().setLegacyId(locationRequest.getLegacyId()); get().setVoided(locationRequest.isVoided()); + get().setGpsCoordinates(locationRequest.getGpsCoordinates()); + get().setLocationProperties(locationRequest.getLocationProperties()); withParentLocation(locationRequest); return this; } diff --git a/avni-server-api/src/main/java/org/avni/server/exporter/v2/ExportV2Processor.java b/avni-server-api/src/main/java/org/avni/server/exporter/v2/ExportV2Processor.java index d388c042f..55dfb8502 100644 --- a/avni-server-api/src/main/java/org/avni/server/exporter/v2/ExportV2Processor.java +++ b/avni-server-api/src/main/java/org/avni/server/exporter/v2/ExportV2Processor.java @@ -36,7 +36,7 @@ public void init() { } @Override - public LongitudinalExportItemRow process(Object exportItem) throws Exception { + public LongitudinalExportItemRow process(Object exportItem) { LongitudinalExportItemRow exportItemRow = new LongitudinalExportItemRow(); Individual individual = initIndividual((Individual) exportItem, exportItemRow); initGeneralEncounters(exportItemRow, individual); diff --git a/avni-server-api/src/main/java/org/avni/server/exporter/v2/LongitudinalExportV2TaskletImpl.java b/avni-server-api/src/main/java/org/avni/server/exporter/v2/LongitudinalExportV2TaskletImpl.java index d86e94322..99db8d021 100644 --- a/avni-server-api/src/main/java/org/avni/server/exporter/v2/LongitudinalExportV2TaskletImpl.java +++ b/avni-server-api/src/main/java/org/avni/server/exporter/v2/LongitudinalExportV2TaskletImpl.java @@ -91,7 +91,7 @@ private void createFileWriter(String uuid, ExecutionContext executionContext) { } private void writeToFile(List rows) throws Exception { - if (rows.size() == 0) return; + if (rows.isEmpty()) return; writer.write(rows); } diff --git a/avni-server-api/src/main/java/org/avni/server/geo/Point.java b/avni-server-api/src/main/java/org/avni/server/geo/Point.java index 1e641f47a..ea74f7c3b 100644 --- a/avni-server-api/src/main/java/org/avni/server/geo/Point.java +++ b/avni-server-api/src/main/java/org/avni/server/geo/Point.java @@ -11,9 +11,12 @@ * @version $Id$ */ public class Point implements Serializable, Cloneable { + private double x; + private double y; - private final double x; - private final double y; + // Required for deserialization + private Point() { + } public Point(double x, double y) { this.x = x; diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java index a84d159c2..e12457406 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java @@ -98,10 +98,10 @@ private Concept findConcept(String name, boolean isChildQuestionGroup) { public ObservationCollection getObservations(Row row, Headers headers, - List errorMsgs, FormType formType, ObservationCollection oldObservations) throws Exception { + List errorMsgs, FormType formType, ObservationCollection oldObservations) { ObservationCollection observationCollection = constructObservations(row, headers, errorMsgs, formType, oldObservations); - if (errorMsgs.size() > 0) { - throw new Exception(String.join(", ", errorMsgs)); + if (!errorMsgs.isEmpty()) { + throw new RuntimeException(String.join(", ", errorMsgs)); } return observationCollection; } @@ -131,7 +131,7 @@ private String getRowValue(FormElement formElement, Row row, Integer questionGro return row.get(concept.getName()); } - private ObservationCollection constructObservations(Row row, Headers headers, List errorMsgs, FormType formType, ObservationCollection oldObservations) throws Exception { + private ObservationCollection constructObservations(Row row, Headers headers, List errorMsgs, FormType formType, ObservationCollection oldObservations) { List observationRequests = new ArrayList<>(); for (Concept concept : getConceptHeaders(headers, row.getHeaders())) { FormElement formElement = getFormElementForObservationConcept(concept, formType); @@ -205,10 +205,10 @@ private List createDecisionFormElement(Set concepts) { }).collect(Collectors.toList()); } - private FormElement getFormElementForObservationConcept(Concept concept, FormType formType) throws Exception { + private FormElement getFormElementForObservationConcept(Concept concept, FormType formType) { List applicableForms = formRepository.findByFormTypeAndIsVoidedFalse(formType); - if (applicableForms.size() == 0) - throw new Exception(String.format("No forms of type %s found", formType)); + if (applicableForms.isEmpty()) + throw new RuntimeException(String.format("No forms of type %s found", formType)); return applicableForms.stream() .map(f -> { @@ -219,7 +219,7 @@ private FormElement getFormElementForObservationConcept(Concept concept, FormTyp .flatMap(List::stream) .filter(fel -> fel.getConcept().equals(concept)) .findFirst() - .orElseThrow(() -> new Exception("No form element linked to concept found")); + .orElseThrow(() -> new RuntimeException("No form element linked to concept found")); } private Object getObservationValue(FormElement formElement, String answerValue, FormType formType, List errorMsgs, Row row, Headers headers, ObservationCollection oldObservations) throws Exception { diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java index df3fbd4b4..bcb08099a 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java @@ -89,7 +89,7 @@ private void strictWriter(Row row, List allErrorMsgs) throws Exception { updateExistingLocation(existingLocation, parent, row, allErrorMsgs, id); } else { AddressLevel location = createAddressLevel(row, parent, locationEntry.getKey()); - updateLocationProperties(row, allErrorMsgs, location, locationEntry.getKey()); + updateLocationProperties(row, allErrorMsgs, location); } } @@ -106,12 +106,12 @@ private void relaxedWriter(Row row, List allErrorMsgs) throws Exception parent = location; } //This will get called only when location have extra properties if (location != null && !this.locationTypeNames.contains(header)) { - updateLocationProperties(row, allErrorMsgs, location, header); + updateLocationProperties(row, allErrorMsgs, location); } } } - private void updateLocationProperties(Row row, List allErrorMsgs, AddressLevel location, String header) throws Exception { + private void updateLocationProperties(Row row, List allErrorMsgs, AddressLevel location) { location.setGpsCoordinates(locationCreator.getLocation(row, headers.gpsCoordinates, allErrorMsgs)); location.setLocationProperties(observationCreator.getObservations(row, headers, allErrorMsgs, FormType.Location, location.getLocationProperties())); locationRepository.save(location); @@ -153,7 +153,7 @@ private void updateExistingLocation(AddressLevel location, AddressLevel parent, location.setParent(parent); location.setLegacyId(id); location.setLineage(lineage); - updateLocationProperties(row, allErrorMsgs, location, header); + updateLocationProperties(row, allErrorMsgs, location); } private Map.Entry ensureAllParentsExist(List allErrorMsgs, List> allNonEmptyLocations) throws Exception { diff --git a/avni-server-api/src/main/java/org/avni/server/service/ObservationService.java b/avni-server-api/src/main/java/org/avni/server/service/ObservationService.java index 0abc4f55a..523d5025b 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ObservationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ObservationService.java @@ -36,16 +36,14 @@ public class ObservationService { private final IndividualRepository individualRepository; private final LocationRepository locationRepository; private final NamedParameterJdbcTemplate jdbcTemplate; - private final FormRepository formRepository; private final EnhancedValidationService enhancedValidationService; @Autowired - public ObservationService(ConceptRepository conceptRepository, IndividualRepository individualRepository, LocationRepository locationRepository, NamedParameterJdbcTemplate jdbcTemplate, FormRepository formRepository, Optional enhancedValidationService) { + public ObservationService(ConceptRepository conceptRepository, IndividualRepository individualRepository, LocationRepository locationRepository, NamedParameterJdbcTemplate jdbcTemplate, Optional enhancedValidationService) { this.conceptRepository = conceptRepository; this.individualRepository = individualRepository; this.locationRepository = locationRepository; this.jdbcTemplate = jdbcTemplate; - this.formRepository = formRepository; this.enhancedValidationService = enhancedValidationService.orElse(null); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java index 6284804aa..b1629ffbb 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java @@ -46,7 +46,7 @@ public class LocationController implements RestControllerResourceProcessor scopeBasedSyncService; private final AccessControlService accessControlService; - private LocationSyncRepository locationSyncRepository; + private final LocationSyncRepository locationSyncRepository; @Autowired public LocationController(LocationRepository locationRepository, UserService userService, LocationService locationService, ScopeBasedSyncService scopeBasedSyncService, AccessControlService accessControlService, LocationSyncRepository locationSyncRepository) { @@ -141,7 +141,7 @@ public ResponseEntity voidLocation(@PathVariable("id") Long id) { if (location == null) return ResponseEntity.badRequest().body(String.format("Location with id '%d' not found", id)); - if (location.getNonVoidedSubLocations().size() > 0) + if (!location.getNonVoidedSubLocations().isEmpty()) return ResponseEntity.badRequest().body(ReactAdminUtil.generateJsonError( String.format("Cannot delete location '%s' until all sub locations are deleted", location.getTitle())) ); diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java index c5e74fe13..c864b7735 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java @@ -1,6 +1,8 @@ package org.avni.server.web.request; import org.avni.server.domain.AddressLevel; +import org.avni.server.domain.ObservationCollection; +import org.avni.server.geo.Point; import org.springframework.util.StringUtils; import java.util.ArrayList; @@ -15,6 +17,8 @@ public class LocationContract extends ReferenceDataContract { private String organisationUUID; private String addressLevelTypeUUID; private String legacyId; + private Point gpsCoordinates; + private ObservationCollection locationProperties = new ObservationCollection(); public LocationContract() { } @@ -86,6 +90,18 @@ public void setLegacyId(String legacyId) { this.legacyId = legacyId; } + public Point getGpsCoordinates() { + return gpsCoordinates; + } + + public void setGpsCoordinates(Point gpsCoordinates) { + this.gpsCoordinates = gpsCoordinates; + } + + public ObservationCollection getLocationProperties() { + return locationProperties; + } + public static LocationContract fromAddressLevel(AddressLevel addressLevel) { LocationContract contract = new LocationContract(); if (addressLevel == null) return contract; @@ -96,6 +112,8 @@ public static LocationContract fromAddressLevel(AddressLevel addressLevel) { contract.setAddressLevelTypeUUID(addressLevel.getType().getUuid()); contract.setVoided(addressLevel.isVoided()); contract.setLegacyId(addressLevel.getLegacyId()); + contract.setGpsCoordinates(addressLevel.getGpsCoordinates()); + contract.locationProperties = addressLevel.getLocationProperties(); AddressLevel parent = addressLevel.getParent(); if (parent != null) { ReferenceDataContract parentContract = new ReferenceDataContract(); diff --git a/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java index c73c2e7bc..84d648c4d 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java @@ -41,12 +41,10 @@ public class ObservationServiceTest { private ObservationService observationService; - private String INDIVIDUAL_UUID = "0a1bf764-4576-4d71-b8ec-25895a113e81"; - @Before public void setup() { initMocks(this); - observationService = new ObservationService(conceptRepository, individualRepository, locationRepository, namedParameterJdbcTemplate, formRepository, Optional.of(enhancedValidationService)); + observationService = new ObservationService(conceptRepository, individualRepository, locationRepository, namedParameterJdbcTemplate, Optional.of(enhancedValidationService)); } @Test diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java index dd47b3590..0449f117f 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestGroupService.java @@ -48,6 +48,12 @@ public void giveViewSubjectPrivilegeTo(Group group, SubjectType ... subjectTypes }); } + public void giveEditSubjectPrivilegeTo(Group group, SubjectType ... subjectTypes) { + Arrays.stream(subjectTypes).forEach(subjectType -> { + this.givePrivilege(group, new TestGroupPrivilegeBuilder().withDefaultValuesForNewEntity().setSubjectType(subjectType).build(), PrivilegeType.EditSubject); + }); + } + public void giveViewProgramPrivilegeTo(Group group, SubjectType subjectType, Program... programs) { Arrays.stream(programs).forEach(program -> { this.givePrivilege(group, new TestGroupPrivilegeBuilder().withDefaultValuesForNewEntity().setSubjectType(subjectType).setProgram(program).build(), PrivilegeType.ViewEnrolmentDetails); From 425d54d88269cef822b79c14ee254aa124dd9f52 Mon Sep 17 00:00:00 2001 From: himeshr Date: Tue, 30 Jul 2024 14:43:50 +0530 Subject: [PATCH 042/129] #759 | change version to resolve flyway migration issues --- ...3__OrganisationStatus.sql => V1_339_2__OrganisationStatus.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename avni-server-api/src/main/resources/db/migration/{V1_343__OrganisationStatus.sql => V1_339_2__OrganisationStatus.sql} (100%) diff --git a/avni-server-api/src/main/resources/db/migration/V1_343__OrganisationStatus.sql b/avni-server-api/src/main/resources/db/migration/V1_339_2__OrganisationStatus.sql similarity index 100% rename from avni-server-api/src/main/resources/db/migration/V1_343__OrganisationStatus.sql rename to avni-server-api/src/main/resources/db/migration/V1_339_2__OrganisationStatus.sql From d64960e5758c6a0e18a45a34e458d6d23b1a0a26 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 30 Jul 2024 14:45:34 +0530 Subject: [PATCH 043/129] #751 - added setter in case used in future --- .../java/org/avni/server/web/request/LocationContract.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java index c864b7735..14edfdf92 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java @@ -102,6 +102,10 @@ public ObservationCollection getLocationProperties() { return locationProperties; } + public void setLocationProperties(ObservationCollection locationProperties) { + this.locationProperties = locationProperties; + } + public static LocationContract fromAddressLevel(AddressLevel addressLevel) { LocationContract contract = new LocationContract(); if (addressLevel == null) return contract; From a1d7b459c79c9b3d4e9ba691067f781da79e3bce Mon Sep 17 00:00:00 2001 From: himeshr Date: Tue, 30 Jul 2024 14:51:36 +0530 Subject: [PATCH 044/129] #751 | Use setLocationProperties --- .../main/java/org/avni/server/web/request/LocationContract.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java index 14edfdf92..839515e36 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/LocationContract.java @@ -117,7 +117,7 @@ public static LocationContract fromAddressLevel(AddressLevel addressLevel) { contract.setVoided(addressLevel.isVoided()); contract.setLegacyId(addressLevel.getLegacyId()); contract.setGpsCoordinates(addressLevel.getGpsCoordinates()); - contract.locationProperties = addressLevel.getLocationProperties(); + contract.setLocationProperties(addressLevel.getLocationProperties()); AddressLevel parent = addressLevel.getParent(); if (parent != null) { ReferenceDataContract parentContract = new ReferenceDataContract(); From 32a9932d7e950e9483a6beb7bf04877770b984b7 Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 31 Jul 2024 13:25:16 +0530 Subject: [PATCH 045/129] #768 | Fix issues with update of identifierUserAssignment --- .../IdentifierUserAssignmentRepository.java | 9 +++++++ .../IdentifierUserAssignmentService.java | 19 +++++++++++++ ...IdentifierUserAssignmentWebController.java | 27 ++++++++++--------- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/IdentifierUserAssignmentRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/IdentifierUserAssignmentRepository.java index 8d1c625c0..8706c7ead 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/IdentifierUserAssignmentRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/IdentifierUserAssignmentRepository.java @@ -46,4 +46,13 @@ default List getOverlappingAssignmentForNonPooledIdent User assignedTo = identifierUserAssignment.getAssignedTo(); return getOverlappingAssignmentWithSamePrefix(assignedTo.getUserSettings().getIdPrefix(), identifierUserAssignment.getIdentifierSource().getId(), identifierUserAssignment.getIdentifierStart(), identifierUserAssignment.getIdentifierEnd(), assignedTo.getId()); } + + default IdentifierUserAssignment updateExistingWithNew(IdentifierUserAssignment existingIdentifierUserAssignment, IdentifierUserAssignment newIdentifierUserAssignment) { + existingIdentifierUserAssignment.setAssignedTo(newIdentifierUserAssignment.getAssignedTo()); + existingIdentifierUserAssignment.setIdentifierSource(newIdentifierUserAssignment.getIdentifierSource()); + existingIdentifierUserAssignment.setIdentifierStart(newIdentifierUserAssignment.getIdentifierStart()); + existingIdentifierUserAssignment.setIdentifierEnd(newIdentifierUserAssignment.getIdentifierEnd()); + existingIdentifierUserAssignment.setVoided(newIdentifierUserAssignment.isVoided()); + return save(existingIdentifierUserAssignment); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/identifier/IdentifierUserAssignmentService.java b/avni-server-api/src/main/java/org/avni/server/service/identifier/IdentifierUserAssignmentService.java index e88ae44b4..224c43b1f 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/identifier/IdentifierUserAssignmentService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/identifier/IdentifierUserAssignmentService.java @@ -36,4 +36,23 @@ public void save(IdentifierUserAssignment identifierUserAssignment) throws Ident identifierUserAssignmentRepository.save(identifierUserAssignment); } } + + public void update(IdentifierUserAssignment existingIdentifierUserAssignment, IdentifierUserAssignment newIdentifierUserAssignment) throws IdentifierOverlappingException, ValidationException { + newIdentifierUserAssignment.validate(); + + IdentifierSource identifierSource = newIdentifierUserAssignment.getIdentifierSource(); + synchronized (identifierSource.getUuid().intern()) { + List overlappingWithAssignments; + if (identifierSource.getType().equals(IdentifierGeneratorType.userPoolBasedIdentifierGenerator)) { + overlappingWithAssignments = identifierUserAssignmentRepository.getOverlappingAssignmentForPooledIdentifier(newIdentifierUserAssignment); + } else { + overlappingWithAssignments = identifierUserAssignmentRepository.getOverlappingAssignmentForNonPooledIdentifier(newIdentifierUserAssignment); + } + if (overlappingWithAssignments.size() > 1 + || (overlappingWithAssignments.size() == 1 && !(overlappingWithAssignments.get(0).getId().equals(existingIdentifierUserAssignment.getId())))) + throw new IdentifierOverlappingException(overlappingWithAssignments); + + identifierUserAssignmentRepository.updateExistingWithNew(existingIdentifierUserAssignment, newIdentifierUserAssignment); + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/IdentifierUserAssignmentWebController.java b/avni-server-api/src/main/java/org/avni/server/web/IdentifierUserAssignmentWebController.java index ae11a81bc..0e6b27902 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/IdentifierUserAssignmentWebController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/IdentifierUserAssignmentWebController.java @@ -17,7 +17,6 @@ import org.avni.server.util.ReactAdminUtil; import org.avni.server.web.request.webapp.IdentifierUserAssignmentContractWeb; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.data.domain.Pageable; import org.springframework.hateoas.PagedResources; @@ -66,12 +65,9 @@ public ResponseEntity getOne(@PathVariable("id") Long id) { @Transactional ResponseEntity saveIdentifierAssignment(@RequestBody IdentifierUserAssignmentContractWeb request) { accessControlService.checkPrivilege(PrivilegeType.EditIdentifierUserAssignment); - IdentifierUserAssignment identifierUserAssignment = new IdentifierUserAssignment(); + IdentifierUserAssignment identifierUserAssignment = getIdentifierUserAssignment(request); + identifierUserAssignment.assignUUID(); - identifierUserAssignment.setAssignedTo(request.getUserId() == null ? null : userRepository.findOne(request.getUserId())); - identifierUserAssignment.setIdentifierSource(request.getIdentifierSourceId() == null ? null : identifierSourceRepository.findOne(request.getIdentifierSourceId())); - identifierUserAssignment.setIdentifierStart(request.getIdentifierStart()); - identifierUserAssignment.setIdentifierEnd(request.getIdentifierEnd()); identifierUserAssignment.setVoided(false); try { identifierUserAssignmentService.save(identifierUserAssignment); @@ -86,18 +82,15 @@ ResponseEntity saveIdentifierAssignment(@RequestBody IdentifierUserAssignmentCon public ResponseEntity updateIdAssignment(@RequestBody IdentifierUserAssignmentContractWeb request, @PathVariable("id") Long id) { accessControlService.checkPrivilege(PrivilegeType.EditIdentifierUserAssignment); - IdentifierUserAssignment identifierUserAssignment = identifierUserAssignmentRepository.findOne(id); - if (identifierUserAssignment == null) + IdentifierUserAssignment existingIdentifierUserAssignment = identifierUserAssignmentRepository.findOne(id); + if (existingIdentifierUserAssignment == null) return ResponseEntity.badRequest() .body(ReactAdminUtil.generateJsonError(String.format("Identifier source with id '%d' not found", id))); - identifierUserAssignment.setAssignedTo(request.getUserId() == null ? null : userRepository.findOne(request.getUserId())); - identifierUserAssignment.setIdentifierSource(request.getIdentifierSourceId() == null ? null : identifierSourceRepository.findOne(request.getIdentifierSourceId())); - identifierUserAssignment.setIdentifierStart(request.getIdentifierStart()); - identifierUserAssignment.setIdentifierEnd(request.getIdentifierEnd()); + IdentifierUserAssignment identifierUserAssignment = getIdentifierUserAssignment(request); identifierUserAssignment.setVoided(request.isVoided()); try { - identifierUserAssignmentService.save(identifierUserAssignment); + identifierUserAssignmentService.update(existingIdentifierUserAssignment, identifierUserAssignment); } catch (IdentifierOverlappingException | ValidationException e) { return WebResponseUtil.createBadRequestResponse(e, logger); } @@ -117,4 +110,12 @@ public ResponseEntity voidIdentifierUserAssignment(@PathVariable("id") Long id) return ResponseEntity.ok(null); } + private IdentifierUserAssignment getIdentifierUserAssignment(IdentifierUserAssignmentContractWeb request) { + IdentifierUserAssignment identifierUserAssignment = new IdentifierUserAssignment(); + identifierUserAssignment.setAssignedTo(request.getUserId() == null ? null : userRepository.findOne(request.getUserId())); + identifierUserAssignment.setIdentifierSource(request.getIdentifierSourceId() == null ? null : identifierSourceRepository.findOne(request.getIdentifierSourceId())); + identifierUserAssignment.setIdentifierStart(request.getIdentifierStart()); + identifierUserAssignment.setIdentifierEnd(request.getIdentifierEnd()); + return identifierUserAssignment; + } } From 1125b3acc77cb718ee458a2f927620b47c078187 Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 31 Jul 2024 13:26:26 +0530 Subject: [PATCH 046/129] #768 | Add null check in get and rename var in void of identifierUserAssignment --- .../web/IdentifierUserAssignmentWebController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/IdentifierUserAssignmentWebController.java b/avni-server-api/src/main/java/org/avni/server/web/IdentifierUserAssignmentWebController.java index 0e6b27902..d450133e6 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/IdentifierUserAssignmentWebController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/IdentifierUserAssignmentWebController.java @@ -56,7 +56,7 @@ public PagedResources> getAll(Page @ResponseBody public ResponseEntity getOne(@PathVariable("id") Long id) { IdentifierUserAssignment identifierUserAssignment = identifierUserAssignmentRepository.findOne(id); - if (identifierUserAssignment.isVoided()) + if (identifierUserAssignment == null || identifierUserAssignment.isVoided()) return ResponseEntity.notFound().build(); return new ResponseEntity<>(IdentifierUserAssignmentContractWeb.fromIdentifierUserAssignment(identifierUserAssignment), HttpStatus.OK); } @@ -101,12 +101,12 @@ public ResponseEntity updateIdAssignment(@RequestBody IdentifierUserAssignmentCo @Transactional public ResponseEntity voidIdentifierUserAssignment(@PathVariable("id") Long id) { accessControlService.checkPrivilege(PrivilegeType.EditIdentifierUserAssignment); - IdentifierUserAssignment identifierSource = identifierUserAssignmentRepository.findOne(id); - if (identifierSource == null) + IdentifierUserAssignment identifierUserAssignment = identifierUserAssignmentRepository.findOne(id); + if (identifierUserAssignment == null) return ResponseEntity.notFound().build(); - identifierSource.setVoided(true); - identifierUserAssignmentRepository.save(identifierSource); + identifierUserAssignment.setVoided(true); + identifierUserAssignmentRepository.save(identifierUserAssignment); return ResponseEntity.ok(null); } From 0ff92e55d971ceb55a50adbae6df66bde8e0a308 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 30 Jul 2024 15:57:17 +0530 Subject: [PATCH 047/129] #764 - bundle upload for dashboard filters --- .../org/avni/server/domain/ReportCard.java | 19 ++++---------- .../domain/app/dashboard/DashboardFilter.java | 2 +- .../avni/server/service/DashboardService.java | 17 +++++++++++++ .../DashboardFilterConfigContract.java | 17 ++++++++++++- .../ObservationBasedFilterContract.java | 25 ++++++++++++++++++- .../reports/DashboardFilterConfigRequest.java | 16 +++--------- .../ObservationBasedFilterRequest.java | 21 +--------------- .../DashboardFilterConfigResponse.java | 6 ++++- .../ObservationBasedFilterResponse.java | 10 +++----- 9 files changed, 75 insertions(+), 58 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java b/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java index 25b1f9f9e..153141c12 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/ReportCard.java @@ -1,6 +1,5 @@ package org.avni.server.domain; - import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.avni.server.web.contract.ValueUnit; import org.hibernate.annotations.BatchSize; @@ -8,7 +7,6 @@ import javax.persistence.*; import javax.validation.constraints.NotNull; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -117,7 +115,7 @@ public void setIconFileS3Key(String iconFileS3Key) { } public List getStandardReportCardInputSubjectTypes() { - return safeGetterForStandardReportCardInput("subjectTypes"); + return standardReportCardInput.getList("subjectTypes"); } public void setStandardReportCardInputSubjectTypes(List subjectTypes) { @@ -125,7 +123,7 @@ public void setStandardReportCardInputSubjectTypes(List subjectTypes) { } public List getStandardReportCardInputPrograms() { - return safeGetterForStandardReportCardInput("programs"); + return standardReportCardInput.getList("programs"); } public void setStandardReportCardInputPrograms(List programs) { @@ -133,26 +131,19 @@ public void setStandardReportCardInputPrograms(List programs) { } public List getStandardReportCardInputEncounterTypes() { - return safeGetterForStandardReportCardInput("encounterTypes"); + return standardReportCardInput.getList("encounterTypes"); } public void setStandardReportCardInputEncounterTypes(List encounterTypes) { standardReportCardInput.with("encounterTypes", encounterTypes); } - private List safeGetterForStandardReportCardInput(String key) { - return standardReportCardInput == null ? new ArrayList() : standardReportCardInput.getList(key); - } - - private void safeSetterForStandardReportCardInput(String key, String value) { - standardReportCardInput.with(key, value); - } - public String getStandardReportCardInputRecentDuration() { return standardReportCardInput.getString("recentDuration"); } public void setStandardReportCardInputRecentDuration(ValueUnit standardReportCardInputRecentDuration) { - safeSetterForStandardReportCardInput("recentDuration", standardReportCardInputRecentDuration.toJSONString()); + String value = standardReportCardInputRecentDuration.toJSONString(); + standardReportCardInput.with("recentDuration", value); } } diff --git a/avni-server-api/src/main/java/org/avni/server/domain/app/dashboard/DashboardFilter.java b/avni-server-api/src/main/java/org/avni/server/domain/app/dashboard/DashboardFilter.java index 5e4144530..16b09c77e 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/app/dashboard/DashboardFilter.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/app/dashboard/DashboardFilter.java @@ -57,7 +57,7 @@ public void setFilterConfig(JsonObject filterConfig) { this.filterConfig = filterConfig; } - public class DashboardFilterConfig { + public static class DashboardFilterConfig { public static final String TypeFieldName = "type"; public static final String SubjectTypeFieldName = "subjectTypeUUID"; public static final String WidgetFieldName = "widget"; diff --git a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java index cd4ee8e32..ae6779634 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java @@ -11,6 +11,7 @@ import org.avni.server.web.request.*; import org.avni.server.web.request.reports.DashboardSectionCardMappingRequest; import org.avni.server.web.request.reports.DashboardSectionWebRequest; +import org.avni.server.web.response.reports.DashboardFilterConfigResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.joda.time.DateTime; @@ -54,6 +55,22 @@ public void uploadDashboard(DashboardBundleContract dashboardContract) { dashboard.setVoided(dashboardContract.isVoided()); Dashboard savedDashboard = dashboardRepository.save(dashboard); uploadDashboardSections(dashboardContract, savedDashboard); + uploadDashboardFilters(dashboardContract, savedDashboard); + } + + private void uploadDashboardFilters(DashboardBundleContract bundleContract, Dashboard dashboard) { + List filters = bundleContract.getFilters(); + for (DashboardFilterResponse bundleFilter : filters) { + DashboardFilter dashboardFilter = dashboardFilterRepository.findByUuid(bundleFilter.getUuid()); + if (dashboardFilter == null) { + dashboardFilter = new DashboardFilter(); + dashboardFilter.setUuid(bundleFilter.getUuid()); + } + dashboardFilter.setName(bundleFilter.getName()); + DashboardFilterConfigResponse bundleFilterConfig = bundleFilter.getFilterConfig(); + dashboardFilter.setFilterConfig(bundleFilterConfig.toJsonObject()); + dashboard.addUpdateFilter(dashboardFilter); + } } private void uploadDashboardSections(DashboardBundleContract dashboardContract, Dashboard dashboard) { diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java index 498198509..98677bcd3 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java @@ -3,7 +3,7 @@ import org.avni.server.domain.JsonObject; import org.avni.server.domain.app.dashboard.DashboardFilter; -public class DashboardFilterConfigContract { +public abstract class DashboardFilterConfigContract { private String type; private String subjectTypeUUID; private String widget; @@ -56,4 +56,19 @@ public JsonObject getJsonObject() { return new JsonObject().with(DashboardFilter.DashboardFilterConfig.SubjectTypeFieldName, subjectTypeUUID); } } + + public JsonObject toJsonObject() { + JsonObject jsonObject = new JsonObject(); + DashboardFilter.FilterType filterType = DashboardFilter.FilterType.valueOf(this.getType()); + jsonObject.with(DashboardFilter.DashboardFilterConfig.TypeFieldName, this.getType()) + .with(DashboardFilter.DashboardFilterConfig.SubjectTypeFieldName, this.getSubjectTypeUUID()) + .with(DashboardFilter.DashboardFilterConfig.WidgetFieldName, this.getWidget()); + if (filterType.equals(DashboardFilter.FilterType.GroupSubject)) + jsonObject.put(DashboardFilter.DashboardFilterConfig.GroupSubjectTypeFilterName, getGroupSubjectTypeFilter().getJsonObject()); + else if (filterType.equals(DashboardFilter.FilterType.Concept)) + jsonObject.put(DashboardFilter.DashboardFilterConfig.ObservationBasedFilterName, getObsverationTypeFilterJsonObject()); + return jsonObject; + } + + protected abstract Object getObsverationTypeFilterJsonObject(); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/ObservationBasedFilterContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/ObservationBasedFilterContract.java index 734e34acc..1d89ef1e0 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/ObservationBasedFilterContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/ObservationBasedFilterContract.java @@ -1,9 +1,13 @@ package org.avni.server.web.contract.reports; +import org.avni.server.domain.JsonObject; +import org.avni.server.domain.app.dashboard.DashboardFilter; + import java.util.ArrayList; import java.util.List; -public class ObservationBasedFilterContract { +public abstract class ObservationBasedFilterContract { + private String scope; private List programUUIDs = new ArrayList<>(); private List encounterTypeUUIDs = new ArrayList<>(); @@ -22,4 +26,23 @@ public List getEncounterTypeUUIDs() { public void setEncounterTypeUUIDs(List encounterTypeUUIDs) { this.encounterTypeUUIDs = encounterTypeUUIDs; } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public JsonObject getJsonObject() { + JsonObject jsonObject = new JsonObject(); + jsonObject.put(DashboardFilter.ObservationBasedFilter.ConceptFieldName, this.getConceptUUID()); + jsonObject.put(DashboardFilter.ObservationBasedFilter.ScopeFieldName, this.getScope()); + jsonObject.put(DashboardFilter.ObservationBasedFilter.ProgramsFieldName, getProgramUUIDs()); + jsonObject.put(DashboardFilter.ObservationBasedFilter.EncounterTypesFieldName, getEncounterTypeUUIDs()); + return jsonObject; + } + + protected abstract String getConceptUUID(); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java index 83ca60d24..49d47ef1c 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java @@ -1,8 +1,6 @@ package org.avni.server.web.request.reports; import com.fasterxml.jackson.annotation.JsonInclude; -import org.avni.server.domain.JsonObject; -import org.avni.server.domain.app.dashboard.DashboardFilter; import org.avni.server.web.contract.DashboardFilterConfigContract; @JsonInclude(JsonInclude.Include.NON_NULL) @@ -17,16 +15,8 @@ public void setObservationBasedFilter(ObservationBasedFilterRequest observationB this.observationBasedFilter = observationBasedFilter; } - public JsonObject toJsonObject() { - JsonObject jsonObject = new JsonObject(); - DashboardFilter.FilterType filterType = DashboardFilter.FilterType.valueOf(this.getType()); - jsonObject.with(DashboardFilter.DashboardFilterConfig.TypeFieldName, this.getType()) - .with(DashboardFilter.DashboardFilterConfig.SubjectTypeFieldName, this.getSubjectTypeUUID()) - .with(DashboardFilter.DashboardFilterConfig.WidgetFieldName, this.getWidget()); - if (filterType.equals(DashboardFilter.FilterType.GroupSubject)) - jsonObject.put(DashboardFilter.DashboardFilterConfig.GroupSubjectTypeFilterName, getGroupSubjectTypeFilter().getJsonObject()); - else if (filterType.equals(DashboardFilter.FilterType.Concept)) - jsonObject.put(DashboardFilter.DashboardFilterConfig.ObservationBasedFilterName, observationBasedFilter.getJsonObject()); - return jsonObject; + @Override + protected Object getObsverationTypeFilterJsonObject() { + return observationBasedFilter.getJsonObject(); } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/reports/ObservationBasedFilterRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/reports/ObservationBasedFilterRequest.java index 277fe4d5c..0ca597e75 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/reports/ObservationBasedFilterRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/reports/ObservationBasedFilterRequest.java @@ -1,13 +1,11 @@ package org.avni.server.web.request.reports; -import org.avni.server.domain.JsonObject; -import org.avni.server.domain.app.dashboard.DashboardFilter; import org.avni.server.web.contract.reports.ObservationBasedFilterContract; public class ObservationBasedFilterRequest extends ObservationBasedFilterContract { private String conceptUUID; - private String scope; + @Override public String getConceptUUID() { return conceptUUID; } @@ -15,21 +13,4 @@ public String getConceptUUID() { public void setConceptUUID(String conceptUUID) { this.conceptUUID = conceptUUID; } - - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } - - public JsonObject getJsonObject() { - JsonObject jsonObject = new JsonObject(); - jsonObject.put(DashboardFilter.ObservationBasedFilter.ConceptFieldName, conceptUUID); - jsonObject.put(DashboardFilter.ObservationBasedFilter.ScopeFieldName, scope); - jsonObject.put(DashboardFilter.ObservationBasedFilter.ProgramsFieldName, getProgramUUIDs()); - jsonObject.put(DashboardFilter.ObservationBasedFilter.EncounterTypesFieldName, getEncounterTypeUUIDs()); - return jsonObject; - } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java b/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java index 3d1e2181a..2bb42b339 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java @@ -1,7 +1,6 @@ package org.avni.server.web.response.reports; import com.fasterxml.jackson.annotation.JsonInclude; -import org.avni.server.domain.app.dashboard.DashboardFilter; import org.avni.server.web.contract.DashboardFilterConfigContract; @JsonInclude(JsonInclude.Include.NON_NULL) @@ -15,4 +14,9 @@ public ObservationBasedFilterResponse getObservationBasedFilter() { public void setObservationBasedFilter(ObservationBasedFilterResponse observationBasedFilter) { this.observationBasedFilter = observationBasedFilter; } + + @Override + protected Object getObsverationTypeFilterJsonObject() { + return observationBasedFilter.getJsonObject(); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterResponse.java b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterResponse.java index feab59b35..b19c09642 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterResponse.java @@ -5,7 +5,6 @@ public class ObservationBasedFilterResponse extends ObservationBasedFilterContract { private ConceptContract concept; - private String scope; public ConceptContract getConcept() { return concept; @@ -15,11 +14,8 @@ public void setConcept(ConceptContract concept) { this.concept = concept; } - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; + @Override + protected String getConceptUUID() { + return concept.getUuid(); } } From 5f2f6ec7d49c4dad57351e9e0fd6cef89e9903d2 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 31 Jul 2024 13:27:30 +0530 Subject: [PATCH 048/129] avniproject/avni-webapp#1293 - set type of the job for user subject type job. check for file type and file name. --- .../server/importer/batch/JobService.java | 2 +- .../batch/csv/ErrorFileCreatorListener.java | 6 ++--- .../UserSubjectTypeCreateTasklet.java | 7 +----- .../server/service/OrganisationService.java | 10 +++++--- .../server/service/SubjectTypeService.java | 1 + .../java/org/avni/server/util/AvniFiles.java | 5 ++-- .../org/avni/server/util/BadRequestError.java | 2 -- .../org/avni/server/web/ImportController.java | 25 +++++++++++-------- 8 files changed, 30 insertions(+), 28 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/JobService.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/JobService.java index 63fd3abc8..fb2061823 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/JobService.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/JobService.java @@ -81,7 +81,7 @@ public JobExecution create(String uuid, String type, String fileName, ObjectInfo .addString("locationUploadMode", locationUploadMode) .addString("locationHierarchy", locationHierarchy) .toJobParameters(); - logger.info(format("Bulkupload initiated! Job{type='%s',uuid='%s',fileName='%s'}", type, uuid, fileName)); + logger.info(format("Bulk upload initiated! Job{type='%s',uuid='%s',fileName='%s'}", type, uuid, fileName)); return type.equals("metadataZip") ? bgJobLauncher.run(importZipJob, parameters) : bgJobLauncher.run(importJob, parameters); } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/ErrorFileCreatorListener.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/ErrorFileCreatorListener.java index d8b738241..613633314 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/ErrorFileCreatorListener.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/ErrorFileCreatorListener.java @@ -77,12 +77,12 @@ public void beforeJob(JobExecution jobExecution) { @Override public void afterJob(JobExecution jobExecution) { - logger.info(format("Bulkupload '%s'! %s", jobExecution.getStatus(), jobInfo)); + logger.info(format("Bulk upload '%s'! %s", jobExecution.getStatus(), jobInfo)); try { ObjectInfo metadata = bulkUploadS3Service.uploadErrorFile(errorFile, uuid); - logger.info(format("Bulkupload '%s'! Check for errors at '%s'", jobExecution.getStatus(), metadata.getKey())); + logger.info(format("Bulk upload '%s'! Check for errors at '%s'", jobExecution.getStatus(), metadata.getKey())); } catch (IOException e) { - logger.error("Unable to create error files in S3 {}", e); + logger.error(String.format("Unable to create error files in S3 %s", e.getMessage()), e); } } } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/userSubjectType/UserSubjectTypeCreateTasklet.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/userSubjectType/UserSubjectTypeCreateTasklet.java index b4a914536..ab061b1d5 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/userSubjectType/UserSubjectTypeCreateTasklet.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/userSubjectType/UserSubjectTypeCreateTasklet.java @@ -21,9 +21,7 @@ @JobScope public class UserSubjectTypeCreateTasklet implements Tasklet { private final SubjectTypeRepository subjectTypeRepository; - private final IndividualRepository individualRepository; private final UserRepository userRepository; - private final UserSubjectRepository userSubjectRepository; private final UserService userService; private static final Logger logger = LoggerFactory.getLogger(UserSubjectTypeCreateTasklet.class); @@ -32,13 +30,10 @@ public class UserSubjectTypeCreateTasklet implements Tasklet { @Autowired public UserSubjectTypeCreateTasklet(SubjectTypeRepository subjectTypeRepository, - IndividualRepository individualRepository, UserRepository userRepository, - UserSubjectRepository userSubjectRepository, UserService userService) { + UserService userService) { this.subjectTypeRepository = subjectTypeRepository; - this.individualRepository = individualRepository; this.userRepository = userRepository; - this.userSubjectRepository = userSubjectRepository; this.userService = userService; } diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index b38ce6cb6..3b55c341a 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -98,7 +98,7 @@ public class OrganisationService { private final VideoRepository videoRepository; private final VideoService videoService; private final CardService cardService; - private final DashboardService dashboardService; + private final DashboardFilterRepository dashboardFilterRepository; private final MenuItemService menuItemService; private final MessagingService messagingService; @@ -224,7 +224,8 @@ public OrganisationService(FormRepository formRepository, OrganisationRepository organisationRepository, ReportCardMapper reportCardMapper, DashboardMapper dashboardMapper, - UserSubjectRepository userSubjectRepository) { + UserSubjectRepository userSubjectRepository, + DashboardFilterRepository dashboardFilterRepository) { this.formRepository = formRepository; this.addressLevelTypeRepository = addressLevelTypeRepository; this.locationRepository = locationRepository; @@ -259,7 +260,7 @@ public OrganisationService(FormRepository formRepository, this.videoRepository = videoRepository; this.videoService = videoService; this.cardService = cardService; - this.dashboardService = dashboardService; + this.dashboardFilterRepository = dashboardFilterRepository; this.menuItemService = menuItemService; this.messagingService = messagingService; this.cardRepository = cardRepository; @@ -732,6 +733,7 @@ public void deleteMetadata() { cardRepository, dashboardSectionRepository, groupDashboardRepository, + dashboardFilterRepository, dashboardRepository, msg91ConfigRepository, genderRepository, @@ -800,4 +802,4 @@ public Organisation getCurrentOrganisation() { return organisationRepository.findOne(organisationId); } -} \ No newline at end of file +} diff --git a/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java b/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java index 3c7e94903..a5e21533e 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java @@ -195,6 +195,7 @@ public void updateSyncAttributesIfRequired(SubjectType subjectType) { UserContext userContext = UserContextHolder.getUserContext(); JobParameters jobParameters = new JobParametersBuilder() .addString("uuid", jobUUID) + .addString("type", String.format("Subjects Create - %s", subjectType.getName()), false) .addString("organisationUUID", userContext.getOrganisationUUID()) .addLong("userId", userContext.getUser().getId()) .addLong("subjectTypeId", subjectType.getId()) diff --git a/avni-server-api/src/main/java/org/avni/server/util/AvniFiles.java b/avni-server-api/src/main/java/org/avni/server/util/AvniFiles.java index ae24ae347..3e85ec99b 100644 --- a/avni-server-api/src/main/java/org/avni/server/util/AvniFiles.java +++ b/avni-server-api/src/main/java/org/avni/server/util/AvniFiles.java @@ -7,6 +7,7 @@ import org.apache.tika.metadata.TikaCoreProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import javax.imageio.ImageIO; @@ -157,11 +158,11 @@ public static Dimension getImageDimension(File imgFile, ImageType type) throws I public static void validateFile(MultipartFile file, List expectedMimeTypes) throws IOException { String fileExtension = fileExtensionMap.getOrDefault(expectedMimeTypes.get(0), ""); validateFileName(file.getOriginalFilename(), fileExtension); - validateMimeTypes(file, expectedMimeTypes); } public static void validateFileName(String fileName, String extension) { + assertTrue(!StringUtils.isEmpty(fileName), "File name is empty"); assertTrue(fileName.split("[.]").length == 2, "Double extension file detected"); assertTrue(fileName.endsWith("." + extension), format("Expected file extension: %s, Got %s", extension, fileName.split("[.]")[1])); } @@ -251,7 +252,7 @@ private static Path zipSlipProtect(ZipEntry zipEntry, Path targetDir) throws IOE return normalizePath; } - private static void assertTrue(boolean value, String errorMessage) { + public static void assertTrue(boolean value, String errorMessage) { if (!value) { throw new BadRequestError(errorMessage); } diff --git a/avni-server-api/src/main/java/org/avni/server/util/BadRequestError.java b/avni-server-api/src/main/java/org/avni/server/util/BadRequestError.java index a89b12740..116573de4 100644 --- a/avni-server-api/src/main/java/org/avni/server/util/BadRequestError.java +++ b/avni-server-api/src/main/java/org/avni/server/util/BadRequestError.java @@ -5,9 +5,7 @@ Returning HTTP 400 Bad Request on throw of this exception is handled by org.avni.web.ErrorInterceptors. */ public class BadRequestError extends RuntimeException { - public BadRequestError(String format, Object... args) { super(String.format(format, args)); } - } diff --git a/avni-server-api/src/main/java/org/avni/server/web/ImportController.java b/avni-server-api/src/main/java/org/avni/server/web/ImportController.java index 1a97bbb33..c53467a4e 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ImportController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ImportController.java @@ -104,14 +104,19 @@ public ResponseEntity getImportTypes() { } @PostMapping("/import/new") - public ResponseEntity doit(@RequestParam MultipartFile file, - @RequestParam String type, - @RequestParam boolean autoApprove, - @RequestParam String locationUploadMode, - @RequestParam String locationHierarchy) throws IOException { - + public ResponseEntity importFile(@RequestParam MultipartFile file, + @RequestParam String type, + @RequestParam boolean autoApprove, + @RequestParam String locationUploadMode, + @RequestParam String locationHierarchy) throws IOException { + accessControlService.checkPrivilege(PrivilegeType.UploadMetadataAndData); - validateFile(file, type.equals("metadataZip") ? ZipFiles : Collections.singletonList("text/csv")); + try { + assertTrue(!StringUtils.isEmpty(type), "File type not provided"); + validateFile(file, type.equals("metadataZip") ? ZipFiles : Collections.singletonList("text/csv")); + } catch (BadRequestError e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } String uuid = UUID.randomUUID().toString(); User user = UserContextHolder.getUserContext().getUser(); @@ -120,13 +125,13 @@ public ResponseEntity doit(@RequestParam MultipartFile file, ObjectInfo storedFileInfo = type.equals("metadataZip") ? bulkUploadS3Service.uploadZip(file, uuid) : bulkUploadS3Service.uploadFile(file, uuid); jobService.create(uuid, type, file.getOriginalFilename(), storedFileInfo, user.getId(), organisation.getUuid(), autoApprove, locationUploadMode, locationHierarchy); } catch (JobParametersInvalidException | JobExecutionAlreadyRunningException | JobInstanceAlreadyCompleteException | JobRestartException e) { - logger.error(format("Bulkupload initiation failed. file:'%s', user:'%s'", file.getOriginalFilename(), user.getUsername()), e); + logger.error(format("Bulk upload initiation failed. file:'%s', user:'%s'", file.getOriginalFilename(), user.getUsername()), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorBodyBuilder.getErrorBody(e)); } catch (IOException e) { - logger.error(format("Bulkupload initiation failed. file:'%s', user:'%s'", file.getOriginalFilename(), user.getUsername()), e); + logger.error(format("Bulk upload initiation failed. file:'%s', user:'%s'", file.getOriginalFilename(), user.getUsername()), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorBodyBuilder.getErrorBody(format("Unable to process file. %s", e.getMessage()))); } catch (Exception e) { - logger.error(format("Bulkupload initiation failed. file:'%s', user:'%s'", file.getOriginalFilename(), user.getUsername()), e); + logger.error(format("Bulk upload initiation failed. file:'%s', user:'%s'", file.getOriginalFilename(), user.getUsername()), e); if (!type.equals("metadataZip")) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(format("%s does not appear to be a valid .csv file.", file.getOriginalFilename())); } From ddaa5241306dbf4c386de6c2666359d57d3625cb Mon Sep 17 00:00:00 2001 From: Joy A Date: Wed, 31 Jul 2024 17:22:09 +0530 Subject: [PATCH 049/129] #747 | /api/subjectMigration/bulk modifications - Support bulk migration by sync concepts - Use batch job - WIP - Track batch job status and result --- .../avni/server/dao/IndividualRepository.java | 3 +- .../batch/model/CustomJobParameter.java | 16 ++++ ...ulkSubjectMigrationBatchConfiguration.java | 69 +++++++++++++++ .../BulkSubjectMigrationJobListener.java | 42 +++++++++ .../BulkSubjectMigrationTasklet.java | 43 +++++++++ .../service/SubjectMigrationService.java | 88 +++++++++++++++++-- .../web/SubjectMigrationController.java | 69 +++++++++++---- .../web/request/SubjectMigrationRequest.java | 22 +++-- 8 files changed, 317 insertions(+), 35 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/importer/batch/model/CustomJobParameter.java create mode 100644 avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationBatchConfiguration.java create mode 100644 avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java create mode 100644 avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java diff --git a/avni-server-api/src/main/java/org/avni/server/dao/IndividualRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/IndividualRepository.java index ff2c4166e..b604ff86e 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/IndividualRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/IndividualRepository.java @@ -1,6 +1,5 @@ package org.avni.server.dao; -import org.apache.poi.ss.formula.functions.T; import org.avni.server.application.projections.WebSearchResultProjection; import org.avni.server.domain.*; import org.avni.server.framework.security.UserContextHolder; @@ -53,6 +52,8 @@ default Page findByName(String name, Pageable pageable) { Page findByIdIn(Long[] ids, Pageable pageable); + List findByUuidInAndAddressLevel(List uuids, AddressLevel addressLevel); + default Specification getFilterSpecForName(String value) { return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> { if (value != null && !value.isEmpty()) { diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/model/CustomJobParameter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/model/CustomJobParameter.java new file mode 100644 index 000000000..bf82a24ce --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/model/CustomJobParameter.java @@ -0,0 +1,16 @@ +package org.avni.server.importer.batch.model; + +import org.springframework.batch.core.JobParameter; + +import java.io.Serializable; + +public class CustomJobParameter extends JobParameter { + private final T customParam; + public CustomJobParameter(T customParam){ + super(""); + this.customParam = customParam; + } + public T getValue(){ + return customParam; + } +} \ No newline at end of file diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationBatchConfiguration.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationBatchConfiguration.java new file mode 100644 index 000000000..66aa81056 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationBatchConfiguration.java @@ -0,0 +1,69 @@ +package org.avni.server.importer.batch.sync.attributes.bulkmigration; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.launch.support.SimpleJobLauncher; +import org.springframework.batch.core.listener.ExecutionContextPromotionListener; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +@EnableBatchProcessing +public class BulkSubjectMigrationBatchConfiguration { + private final JobBuilderFactory jobBuilderFactory; + private final StepBuilderFactory stepBuilderFactory; + private final JobRepository jobRepository; + + @Autowired + public BulkSubjectMigrationBatchConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, JobRepository jobRepository) { + this.jobBuilderFactory = jobBuilderFactory; + this.stepBuilderFactory = stepBuilderFactory; + this.jobRepository = jobRepository; + } + + @Bean + public JobLauncher bulkSubjectMigrationJobLauncher() { + return new SimpleJobLauncher() {{ + setJobRepository(jobRepository); + setTaskExecutor(new ThreadPoolTaskExecutor() {{ + setCorePoolSize(1); + setMaxPoolSize(1); + setQueueCapacity(100); + initialize(); + }}); + }}; + } + + @Bean + public Job bulkSubjectMigrationJob(BulkSubjectMigrationJobListener bulkSubjectMigrationJobListener, Step bulkSubjectMigrationStep) { + return jobBuilderFactory + .get("bulkSubjectMigrationJob") + .incrementer(new RunIdIncrementer()) + .listener(bulkSubjectMigrationJobListener) + .start(bulkSubjectMigrationStep) + .build(); + } + + @Bean + public ExecutionContextPromotionListener promotionListener() { + ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener(); + listener.setKeys(new String[] {"data"}); + return listener; + } + + @Bean + public Step bulkSubjectMigrationStep(BulkSubjectMigrationTasklet bulkSubjectMigrationTasklet) { + return stepBuilderFactory.get("bulkSubjectMigrationStep") + .tasklet(bulkSubjectMigrationTasklet) + .listener(promotionListener()) + .build(); + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java new file mode 100644 index 000000000..4c505ed19 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java @@ -0,0 +1,42 @@ +package org.avni.server.importer.batch.sync.attributes.bulkmigration; + +import org.avni.server.framework.security.AuthService; +import org.avni.server.web.request.SubjectMigrationRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.configuration.annotation.JobScope; +import org.springframework.batch.core.listener.JobExecutionListenerSupport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@JobScope +public class BulkSubjectMigrationJobListener extends JobExecutionListenerSupport { + private static final Logger logger = LoggerFactory.getLogger(BulkSubjectMigrationJobListener.class); + private final AuthService authService; + + @Value("#{jobParameters['uuid']}") + private String uuid; + + @Value("#{jobParameters['organisationUUID']}") + private String organisationUUID; + + @Value("#{jobParameters['userId']}") + private Long userId; + + @Value("#{jobParameters['bulkSubjectMigrationParameters']}") + private SubjectMigrationRequest bulkSubjectMigrationParameters; + + @Autowired + public BulkSubjectMigrationJobListener(AuthService authService) { + this.authService = authService; + } + + @Override + public void beforeJob(JobExecution jobExecution) { + logger.info("Starting bulk subject migration job with uuid {}", jobExecution.getJobParameters().getString("uuid")); + authService.authenticateByUserId(userId, organisationUUID); + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java new file mode 100644 index 000000000..b8a847dc3 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java @@ -0,0 +1,43 @@ +package org.avni.server.importer.batch.sync.attributes.bulkmigration; + +import org.avni.server.service.SubjectMigrationService; +import org.avni.server.web.request.SubjectMigrationRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.configuration.annotation.JobScope; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Set; +import java.util.stream.Collectors; + +@Component +@JobScope +public class BulkSubjectMigrationTasklet implements Tasklet { + private static final Logger logger = LoggerFactory.getLogger(BulkSubjectMigrationTasklet.class); + private final SubjectMigrationService subjectMigrationService; + @Value("#{jobParameters['mode']}") + String mode; + + @Value("#{jobParameters['bulkSubjectMigrationParameters']}") + SubjectMigrationRequest bulkSubjectMigrationParameters; + + @Autowired + public BulkSubjectMigrationTasklet(SubjectMigrationService subjectMigrationService) { + this.subjectMigrationService = subjectMigrationService; + } + + @Override + public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception { + logger.info(bulkSubjectMigrationParameters.getSubjectUuids().toString()); + Set migrationCompletedSubjectUuids = subjectMigrationService.bulkMigrate(SubjectMigrationService.BulkSubjectMigrationModes.valueOf(mode), bulkSubjectMigrationParameters); + Set migrationFailedSubjectUuids = bulkSubjectMigrationParameters.getSubjectUuids().stream().filter(s -> !migrationCompletedSubjectUuids.contains(s)).collect(Collectors.toSet()); + logger.info("Failed to migrate subject uuids: {}", migrationFailedSubjectUuids); + return RepeatStatus.FINISHED; + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java index 510ac7d2f..f9fc7dcbf 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java @@ -4,20 +4,22 @@ import org.avni.server.dao.individualRelationship.IndividualRelationshipRepository; import org.avni.server.dao.sync.SyncEntityName; import org.avni.server.domain.*; +import org.avni.server.domain.accessControl.PrivilegeType; import org.avni.server.framework.security.UserContextHolder; +import org.avni.server.service.accessControl.AccessControlService; import org.avni.server.web.IndividualController; +import org.avni.server.web.request.SubjectMigrationRequest; import org.joda.time.DateTime; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.transaction.Transactional; -import java.util.List; -import java.util.Objects; +import java.util.*; @Service public class SubjectMigrationService implements ScopeAwareService { - private static org.slf4j.Logger logger = LoggerFactory.getLogger(IndividualController.class); + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(IndividualController.class); private final EntityApprovalStatusRepository entityApprovalStatusRepository; private final SubjectMigrationRepository subjectMigrationRepository; private final SubjectTypeRepository subjectTypeRepository; @@ -30,6 +32,14 @@ public class SubjectMigrationService implements ScopeAwareService subjects, AddressLevel destAddressLevel) { - subjects.forEach(individual -> { - this.markSubjectMigrationIfRequired(individual.getUuid(), null, destAddressLevel, null, individual.getObservations(), true); - individual.setAddressLevel(destAddressLevel); - individualRepository.saveEntity(individual); + public void changeSubjectAddressLevel(Individual subject, AddressLevel destAddressLevel) { + this.markSubjectMigrationIfRequired(subject.getUuid(), null, destAddressLevel, null, subject.getObservations(), true); + subject.setAddressLevel(destAddressLevel); + individualRepository.saveEntity(subject); + } + + public Set bulkMigrate(BulkSubjectMigrationModes mode, SubjectMigrationRequest subjectMigrationRequest) { + if (mode == BulkSubjectMigrationModes.byAddress) { + return bulkMigrateByAddress(subjectMigrationRequest.getSubjectUuids(), subjectMigrationRequest.getDestinationAddresses()); + } else { + return bulkMigrateBySyncConcept(subjectMigrationRequest.getSubjectUuids(), subjectMigrationRequest.getDestinationSyncConcepts()); + } + } + + public Set bulkMigrateByAddress(List subjectUuids, Map destinationAddresses) { +// Set migrationFailedSubjectUuids = new HashSet<>(); + Set migrationCompletedSubjectUuids = new HashSet<>(); + for (Map.Entry destinationAddressEntry : destinationAddresses.entrySet()) { + Long source = Long.parseLong(destinationAddressEntry.getKey()); + Long dest = Long.parseLong(destinationAddressEntry.getValue()); + + AddressLevel sourceAddressLevel = locationRepository.findOne(source); + AddressLevel destAddressLevel = locationRepository.findOne(dest); + if (sourceAddressLevel == null || destAddressLevel == null) continue; + + List subjectsByAddressLevel = individualRepository.findByUuidInAndAddressLevel(subjectUuids, sourceAddressLevel); + + subjectsByAddressLevel.forEach(subject -> { + try { + accessControlService.checkSubjectPrivilege(PrivilegeType.EditSubject, subject.getSubjectType().getUuid()); + logger.info(String.format("Migrating subject : %s, for source address: %s, to destination address: %s", subject.getUuid(), source, dest)); + this.changeSubjectAddressLevel(subject, destAddressLevel); + migrationCompletedSubjectUuids.add(subject.getUuid()); + } catch (Exception e) { +// migrationFailedSubjectUuids.add(subject.getUuid()); + } + }); + } + return migrationCompletedSubjectUuids; + } + + public Set bulkMigrateBySyncConcept(List subjectUuids, Map destinationSyncConcepts) { + Set migrationCompletedSubjectUuids = new HashSet<>(); + subjectUuids.forEach(subjectUuid -> { + try { + Individual subject = individualRepository.findByUuid(subjectUuid); + if (subject == null) return; + accessControlService.checkSubjectPrivilege(PrivilegeType.EditSubject, subject.getSubjectType().getUuid()); + String destinationSyncConcept1Value = destinationSyncConcepts.get(subject.getSubjectType().getSyncRegistrationConcept1()); + String destinationSyncConcept2Value = destinationSyncConcepts.get(subject.getSubjectType().getSyncRegistrationConcept2()); + if (destinationSyncConcept1Value == null && destinationSyncConcept2Value == null) return; + ObservationCollection newObservations = new ObservationCollection(); + if (destinationSyncConcept1Value != null) { + newObservations.put(subject.getSubjectType().getSyncRegistrationConcept1(), destinationSyncConcept1Value); + } + if (destinationSyncConcept2Value != null) { + newObservations.put(subject.getSubjectType().getSyncRegistrationConcept2(), destinationSyncConcept2Value); + } + this.markSubjectMigrationIfRequired(subjectUuid, null, null, null, newObservations, true); + migrationCompletedSubjectUuids.add(subject.getUuid()); + } catch (Exception e) { + } }); + return migrationCompletedSubjectUuids; } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java index ad7bcb411..a13877e6b 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java @@ -1,18 +1,32 @@ package org.avni.server.web; -import org.avni.server.dao.*; +import org.avni.server.dao.IndividualRepository; +import org.avni.server.dao.LocationRepository; +import org.avni.server.dao.SubjectMigrationRepository; +import org.avni.server.dao.SubjectTypeRepository; import org.avni.server.dao.sync.SyncEntityName; import org.avni.server.domain.*; import org.avni.server.domain.accessControl.PrivilegeType; +import org.avni.server.framework.security.UserContextHolder; +import org.avni.server.importer.batch.model.CustomJobParameter; import org.avni.server.service.ScopeBasedSyncService; import org.avni.server.service.SubjectMigrationService; import org.avni.server.service.UserService; import org.avni.server.service.accessControl.AccessControlService; +import org.avni.server.util.BadRequestError; import org.avni.server.web.request.SubjectMigrationRequest; import org.avni.server.web.response.slice.SlicedResources; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; +import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; +import org.springframework.batch.core.repository.JobRestartException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -21,12 +35,13 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.PagedResources; import org.springframework.hateoas.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.UUID; import static org.avni.server.web.resourceProcessors.ResourceProcessor.addAuditFields; @@ -41,14 +56,18 @@ public class SubjectMigrationController extends AbstractController scopeBasedSyncService, SubjectMigrationService subjectMigrationService, IndividualRepository individualRepository, LocationRepository locationRepository, AccessControlService accessControlService) { + public SubjectMigrationController(SubjectMigrationRepository subjectMigrationRepository, SubjectTypeRepository subjectTypeRepository, UserService userService, ScopeBasedSyncService scopeBasedSyncService, SubjectMigrationService subjectMigrationService, IndividualRepository individualRepository, LocationRepository locationRepository, AccessControlService accessControlService, Job bulkSubjectMigrationJob, JobLauncher bulkSubjectMigrationJobLauncher) { this.scopeBasedSyncService = scopeBasedSyncService; this.subjectMigrationService = subjectMigrationService; this.individualRepository = individualRepository; this.locationRepository = locationRepository; this.accessControlService = accessControlService; + this.bulkSubjectMigrationJob = bulkSubjectMigrationJob; + this.bulkSubjectMigrationJobLauncher = bulkSubjectMigrationJobLauncher; logger = LoggerFactory.getLogger(this.getClass()); this.subjectMigrationRepository = subjectMigrationRepository; this.subjectTypeRepository = subjectTypeRepository; @@ -99,24 +118,36 @@ public Resource process(Resource resource) { @RequestMapping(value = "/api/subjectMigration/bulk", method = RequestMethod.POST) @PreAuthorize(value = "hasAnyAuthority('user')") - public void migrate(@RequestBody SubjectMigrationRequest subjectMigrationRequest) { + public ResponseEntity migrate(@RequestParam(value = "mode", defaultValue = "byAddress") SubjectMigrationService.BulkSubjectMigrationModes mode, + @RequestBody SubjectMigrationRequest subjectMigrationRequest) { accessControlService.checkPrivilege(PrivilegeType.MultiTxEntityTypeUpdate); - Map destinationAddresses = subjectMigrationRequest.getDestinationAddresses(); - - for (Map.Entry destinationAddressEntry : destinationAddresses.entrySet()) { - Long source = Long.parseLong(destinationAddressEntry.getKey()); - Long dest = Long.parseLong(destinationAddressEntry.getValue()); + if (mode == SubjectMigrationService.BulkSubjectMigrationModes.byAddress && subjectMigrationRequest.getDestinationAddresses() == null) { + throw new BadRequestError("destinationAddresses is required for mode: byAddress"); + } + if (mode == SubjectMigrationService.BulkSubjectMigrationModes.bySyncConcept && subjectMigrationRequest.getDestinationSyncConcepts() == null) { + throw new BadRequestError("destinationSyncConcepts is required for mode: bySyncConcepts"); + } - AddressLevel sourceAddressLevel = locationRepository.findOne(source); - AddressLevel destAddressLevel = locationRepository.findOne(dest); + UserContext userContext = UserContextHolder.getUserContext(); + User user = userContext.getUser(); + Organisation organisation = userContext.getOrganisation(); + String jobUUID = UUID.randomUUID().toString(); + JobParameters jobParameters = + new JobParametersBuilder() + .addString("uuid", jobUUID) + .addString("organisationUUID", organisation.getUuid()) + .addLong("userId", user.getId(), false) + .addString("mode", String.valueOf(mode)) + .addParameter("bulkSubjectMigrationParameters", new CustomJobParameter<>(subjectMigrationRequest)) + .toJobParameters(); - subjectMigrationRequest.getSubjectTypeIds().forEach(subjectTypeId -> { - SubjectType subjectType = subjectTypeRepository.findOne(subjectTypeId); - accessControlService.checkSubjectPrivilege(PrivilegeType.EditSubject, subjectType.getUuid()); - List subjects = individualRepository.findAllByAddressLevelAndSubjectType(sourceAddressLevel, subjectType); - logger.info(String.format("Migrating for subject type: %s, for source address: %s, to destination address: %s, containing %d subjects", subjectType.getName(), source, dest, subjects.size())); - subjectMigrationService.changeSubjectsAddressLevel(subjects, destAddressLevel); - }); + try { + bulkSubjectMigrationJobLauncher.run(bulkSubjectMigrationJob, jobParameters); + } catch (JobParametersInvalidException | JobExecutionAlreadyRunningException | + JobInstanceAlreadyCompleteException | JobRestartException e) { + throw new RuntimeException(String.format("Error while starting the bulk subject migration job, %s", e.getMessage()), e); } + + return ResponseEntity.status(HttpStatus.ACCEPTED).body(jobUUID); } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java index 72932ae4a..ec763045b 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java @@ -1,18 +1,20 @@ package org.avni.server.web.request; +import java.io.Serializable; import java.util.List; import java.util.Map; -public class SubjectMigrationRequest { +public class SubjectMigrationRequest implements Serializable { + private List subjectUuids; private Map destinationAddresses; - private List subjectTypeIds; + private Map destinationSyncConcepts; - public List getSubjectTypeIds() { - return subjectTypeIds; + public List getSubjectUuids() { + return subjectUuids; } - public void setSubjectTypeIds(List subjectTypeIds) { - this.subjectTypeIds = subjectTypeIds; + public void setSubjectUuids(List subjectUuids) { + this.subjectUuids = subjectUuids; } public Map getDestinationAddresses() { @@ -22,4 +24,12 @@ public Map getDestinationAddresses() { public void setDestinationAddresses(Map destinationAddresses) { this.destinationAddresses = destinationAddresses; } + + public Map getDestinationSyncConcepts() { + return destinationSyncConcepts; + } + + public void setDestinationSyncConcepts(Map destinationSyncConcepts) { + this.destinationSyncConcepts = destinationSyncConcepts; + } } From 64e58e48faccea791b825a12784e36bd1319ca17 Mon Sep 17 00:00:00 2001 From: Joy A Date: Thu, 1 Aug 2024 21:59:07 +0530 Subject: [PATCH 050/129] #747 | /api/subjectMigration/bulk and status endpoints --- .../avni/server/dao/IndividualRepository.java | 2 - ...ulkSubjectMigrationBatchConfiguration.java | 9 -- .../BulkSubjectMigrationJobListener.java | 11 +- .../BulkSubjectMigrationTasklet.java | 25 ++- .../service/SubjectMigrationService.java | 147 ++++++++++++------ .../web/SubjectMigrationController.java | 25 ++- .../web/request/SubjectMigrationRequest.java | 10 +- .../src/main/resources/api/external-api.yaml | 94 ++++++++++- 8 files changed, 234 insertions(+), 89 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/IndividualRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/IndividualRepository.java index b604ff86e..1c0f91763 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/IndividualRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/IndividualRepository.java @@ -52,8 +52,6 @@ default Page findByName(String name, Pageable pageable) { Page findByIdIn(Long[] ids, Pageable pageable); - List findByUuidInAndAddressLevel(List uuids, AddressLevel addressLevel); - default Specification getFilterSpecForName(String value) { return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> { if (value != null && !value.isEmpty()) { diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationBatchConfiguration.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationBatchConfiguration.java index 66aa81056..cc800bfc3 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationBatchConfiguration.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationBatchConfiguration.java @@ -8,7 +8,6 @@ import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.launch.support.SimpleJobLauncher; -import org.springframework.batch.core.listener.ExecutionContextPromotionListener; import org.springframework.batch.core.repository.JobRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -52,18 +51,10 @@ public Job bulkSubjectMigrationJob(BulkSubjectMigrationJobListener bulkSubjectMi .build(); } - @Bean - public ExecutionContextPromotionListener promotionListener() { - ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener(); - listener.setKeys(new String[] {"data"}); - return listener; - } - @Bean public Step bulkSubjectMigrationStep(BulkSubjectMigrationTasklet bulkSubjectMigrationTasklet) { return stepBuilderFactory.get("bulkSubjectMigrationStep") .tasklet(bulkSubjectMigrationTasklet) - .listener(promotionListener()) .build(); } } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java index 4c505ed19..702aeae0f 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java @@ -20,6 +20,9 @@ public class BulkSubjectMigrationJobListener extends JobExecutionListenerSupport @Value("#{jobParameters['uuid']}") private String uuid; + @Value("#{jobParameters['mode']}") + private String mode; + @Value("#{jobParameters['organisationUUID']}") private String organisationUUID; @@ -36,7 +39,13 @@ public BulkSubjectMigrationJobListener(AuthService authService) { @Override public void beforeJob(JobExecution jobExecution) { - logger.info("Starting bulk subject migration job with uuid {}", jobExecution.getJobParameters().getString("uuid")); + logger.info("Starting Bulk Subject Migration Job {} mode: {}. Migrating {} subjects", uuid, mode, bulkSubjectMigrationParameters.getSubjectIds().size()); authService.authenticateByUserId(userId, organisationUUID); } + + @Override + public void afterJob(JobExecution jobExecution) { + logger.info("Finished Bulk Subject Migration Job {} mode: {} exitStatus: {} createTime: {} startTime: {} endTime: {}", + uuid, mode, jobExecution.getExitStatus(), jobExecution.getCreateTime(), jobExecution.getStartTime(), jobExecution.getEndTime()); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java index b8a847dc3..153c67a96 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java @@ -1,6 +1,9 @@ package org.avni.server.importer.batch.sync.attributes.bulkmigration; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.avni.server.service.S3Service; import org.avni.server.service.SubjectMigrationService; +import org.avni.server.util.ObjectMapperSingleton; import org.avni.server.web.request.SubjectMigrationRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,14 +16,18 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.util.Set; -import java.util.stream.Collectors; +import java.io.File; +import java.util.Map; @Component @JobScope public class BulkSubjectMigrationTasklet implements Tasklet { private static final Logger logger = LoggerFactory.getLogger(BulkSubjectMigrationTasklet.class); private final SubjectMigrationService subjectMigrationService; + private final S3Service s3Service; + @Value("#{jobParameters['uuid']}") + String uuid; + @Value("#{jobParameters['mode']}") String mode; @@ -28,16 +35,20 @@ public class BulkSubjectMigrationTasklet implements Tasklet { SubjectMigrationRequest bulkSubjectMigrationParameters; @Autowired - public BulkSubjectMigrationTasklet(SubjectMigrationService subjectMigrationService) { + public BulkSubjectMigrationTasklet(SubjectMigrationService subjectMigrationService, S3Service s3Service) { this.subjectMigrationService = subjectMigrationService; + this.s3Service = s3Service; } @Override public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception { - logger.info(bulkSubjectMigrationParameters.getSubjectUuids().toString()); - Set migrationCompletedSubjectUuids = subjectMigrationService.bulkMigrate(SubjectMigrationService.BulkSubjectMigrationModes.valueOf(mode), bulkSubjectMigrationParameters); - Set migrationFailedSubjectUuids = bulkSubjectMigrationParameters.getSubjectUuids().stream().filter(s -> !migrationCompletedSubjectUuids.contains(s)).collect(Collectors.toSet()); - logger.info("Failed to migrate subject uuids: {}", migrationFailedSubjectUuids); + Map failedMigrations = subjectMigrationService.bulkMigrate(SubjectMigrationService.BulkSubjectMigrationModes.valueOf(mode), bulkSubjectMigrationParameters); + ObjectMapper objectMapper = ObjectMapperSingleton.getObjectMapper(); + String fileName = uuid + ".json"; + File failedMigrationsFile = new File("/tmp/" + fileName); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(failedMigrationsFile, failedMigrations); +// TODO upload file to S3 +// s3Service.uploadFile(failedMigrationsFile, fileName, "bulkuploads/subjectmigrations"); return RepeatStatus.FINISHED; } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java index f9fc7dcbf..59d41ba9a 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java @@ -34,6 +34,8 @@ public class SubjectMigrationService implements ScopeAwareService %s", subject.getUuid(), subject.getAddressLevel().getId(), destAddressLevel.getId())); this.markSubjectMigrationIfRequired(subject.getUuid(), null, destAddressLevel, null, subject.getObservations(), true); subject.setAddressLevel(destAddressLevel); - individualRepository.saveEntity(subject); + individualService.save(subject); } - public Set bulkMigrate(BulkSubjectMigrationModes mode, SubjectMigrationRequest subjectMigrationRequest) { + @Transactional + public void changeSubjectSyncConceptValues(Individual subject, String destinationSyncConcept1Value, String destinationSyncConcept2Value) { + logger.info(String.format("Migrating subject: '%s', sync concept 1 value: '%s' -> '%s', sync concept 2 value: '%s' -> '%s'", subject.getUuid(), subject.getSyncConcept1Value(), destinationSyncConcept1Value, subject.getSyncConcept2Value(), destinationSyncConcept2Value)); + ObservationCollection newObservations = buildSyncConceptValueObservations(subject, destinationSyncConcept1Value, destinationSyncConcept2Value); + this.markSubjectMigrationIfRequired(subject.getUuid(), null, subject.getAddressLevel(), null, newObservations, true); + subject.addObservations(newObservations); + individualService.save(subject); + } + + public Map bulkMigrate(BulkSubjectMigrationModes mode, SubjectMigrationRequest subjectMigrationRequest) { if (mode == BulkSubjectMigrationModes.byAddress) { - return bulkMigrateByAddress(subjectMigrationRequest.getSubjectUuids(), subjectMigrationRequest.getDestinationAddresses()); + return bulkMigrateByAddress(subjectMigrationRequest.getSubjectIds(), subjectMigrationRequest.getDestinationAddresses()); } else { - return bulkMigrateBySyncConcept(subjectMigrationRequest.getSubjectUuids(), subjectMigrationRequest.getDestinationSyncConcepts()); + return bulkMigrateBySyncConcept(subjectMigrationRequest.getSubjectIds(), subjectMigrationRequest.getDestinationSyncConcepts()); } } - public Set bulkMigrateByAddress(List subjectUuids, Map destinationAddresses) { -// Set migrationFailedSubjectUuids = new HashSet<>(); - Set migrationCompletedSubjectUuids = new HashSet<>(); + public Map bulkMigrateByAddress(List subjectIds, Map destinationAddresses) { + Map migrationFailures = new HashMap<>(); + Map addressLevelMap = new HashMap<>(); for (Map.Entry destinationAddressEntry : destinationAddresses.entrySet()) { - Long source = Long.parseLong(destinationAddressEntry.getKey()); - Long dest = Long.parseLong(destinationAddressEntry.getValue()); - - AddressLevel sourceAddressLevel = locationRepository.findOne(source); - AddressLevel destAddressLevel = locationRepository.findOne(dest); - if (sourceAddressLevel == null || destAddressLevel == null) continue; - - List subjectsByAddressLevel = individualRepository.findByUuidInAndAddressLevel(subjectUuids, sourceAddressLevel); - - subjectsByAddressLevel.forEach(subject -> { - try { - accessControlService.checkSubjectPrivilege(PrivilegeType.EditSubject, subject.getSubjectType().getUuid()); - logger.info(String.format("Migrating subject : %s, for source address: %s, to destination address: %s", subject.getUuid(), source, dest)); - this.changeSubjectAddressLevel(subject, destAddressLevel); - migrationCompletedSubjectUuids.add(subject.getUuid()); - } catch (Exception e) { -// migrationFailedSubjectUuids.add(subject.getUuid()); + try { + Long source = Long.parseLong(destinationAddressEntry.getKey()); + Long dest = Long.parseLong(destinationAddressEntry.getValue()); + + AddressLevel sourceAddressLevel = locationRepository.findOne(source); + AddressLevel destAddressLevel = locationRepository.findOne(dest); + if (sourceAddressLevel != null && destAddressLevel != null) { + addressLevelMap.put(sourceAddressLevel, destAddressLevel); } - }); + } catch (NumberFormatException e) { + //Continue with other destinationAddresses + } } - return migrationCompletedSubjectUuids; + subjectIds.forEach(subjectId -> { + try { + Individual subject = individualRepository.findOne(subjectId); + if (subject == null) throw new RuntimeException("Subject not found"); + + accessControlService.checkSubjectPrivilege(PrivilegeType.EditSubject, subject.getSubjectType().getUuid()); + AddressLevel destAddressLevel = addressLevelMap.get(subject.getAddressLevel()); + if (destAddressLevel == null || destAddressLevel.isVoided()) throw new RuntimeException("Destination address level unavailable / voided"); + this.changeSubjectAddressLevel(subject, destAddressLevel); + } catch (Exception e) { + logger.debug("Failed to migrate subject {} byAddress", subjectId); + migrationFailures.put(String.valueOf(subjectId), e.getMessage()); + } + }); + return migrationFailures; } - public Set bulkMigrateBySyncConcept(List subjectUuids, Map destinationSyncConcepts) { - Set migrationCompletedSubjectUuids = new HashSet<>(); - subjectUuids.forEach(subjectUuid -> { + public Map bulkMigrateBySyncConcept(List subjectIds, Map destinationSyncConcepts) { + Map migrationFailures = new HashMap<>(); + subjectIds.forEach(subjectId -> { try { - Individual subject = individualRepository.findByUuid(subjectUuid); - if (subject == null) return; + Individual subject = individualRepository.findOne(subjectId); + if (subject == null) throw new RuntimeException("Subject not found"); accessControlService.checkSubjectPrivilege(PrivilegeType.EditSubject, subject.getSubjectType().getUuid()); - String destinationSyncConcept1Value = destinationSyncConcepts.get(subject.getSubjectType().getSyncRegistrationConcept1()); - String destinationSyncConcept2Value = destinationSyncConcepts.get(subject.getSubjectType().getSyncRegistrationConcept2()); - if (destinationSyncConcept1Value == null && destinationSyncConcept2Value == null) return; - ObservationCollection newObservations = new ObservationCollection(); - if (destinationSyncConcept1Value != null) { - newObservations.put(subject.getSubjectType().getSyncRegistrationConcept1(), destinationSyncConcept1Value); - } - if (destinationSyncConcept2Value != null) { - newObservations.put(subject.getSubjectType().getSyncRegistrationConcept2(), destinationSyncConcept2Value); + String destinationSyncConcept1Value = validateSyncConcept(subject.getSubjectType().getSyncRegistrationConcept1(), subject.getSyncConcept1Value(), destinationSyncConcepts); + String destinationSyncConcept2Value = validateSyncConcept(subject.getSubjectType().getSyncRegistrationConcept2(), subject.getSyncConcept2Value(), destinationSyncConcepts); + if (destinationSyncConcept1Value == null && destinationSyncConcept2Value == null) { + throw new RuntimeException("Valid destination sync concept(s) not found"); } - this.markSubjectMigrationIfRequired(subjectUuid, null, null, null, newObservations, true); - migrationCompletedSubjectUuids.add(subject.getUuid()); + changeSubjectSyncConceptValues(subject, destinationSyncConcept1Value, destinationSyncConcept2Value); } catch (Exception e) { + logger.debug("Failed to migrate subject {} bySyncConcept", subjectId); + migrationFailures.put(String.valueOf(subjectId), e.getMessage()); } }); - return migrationCompletedSubjectUuids; + return migrationFailures; + } + + private String validateSyncConcept(String subjectTypeSyncConceptUuid, String currentValue, Map destinationSyncConcepts) { + if (subjectTypeSyncConceptUuid == null) { + throw new RuntimeException("No sync concept configured for subject type"); + } + Concept syncConcept = conceptRepository.findByUuid(subjectTypeSyncConceptUuid); + + String destinationSyncConceptValue = destinationSyncConcepts.get(subjectTypeSyncConceptUuid); + + if (Objects.equals(currentValue, destinationSyncConceptValue)) { + throw new RuntimeException("Source value and Destination value are the same"); + } + + if (destinationSyncConceptValue != null && syncConcept.isCoded()) { + ConceptAnswer conceptAnswer = syncConcept.findConceptAnswerByConceptUUID(destinationSyncConceptValue); + if (conceptAnswer == null || conceptAnswer.isVoided()) { + throw new RuntimeException(String.format("Invalid value '%s' for coded sync concept", destinationSyncConceptValue)); + } + } + return destinationSyncConceptValue; + } + + private static ObservationCollection buildSyncConceptValueObservations(Individual subject, String destinationSyncConcept1Value, String destinationSyncConcept2Value) { + ObservationCollection newObservations = new ObservationCollection(); + if (destinationSyncConcept1Value != null) { + newObservations.put(subject.getSubjectType().getSyncRegistrationConcept1(), destinationSyncConcept1Value.trim()); + } + if (destinationSyncConcept2Value != null) { + newObservations.put(subject.getSubjectType().getSyncRegistrationConcept2(), destinationSyncConcept2Value.trim()); + } + return newObservations; } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java index a13877e6b..28501648d 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java @@ -1,9 +1,6 @@ package org.avni.server.web; -import org.avni.server.dao.IndividualRepository; -import org.avni.server.dao.LocationRepository; -import org.avni.server.dao.SubjectMigrationRepository; -import org.avni.server.dao.SubjectTypeRepository; +import org.avni.server.dao.*; import org.avni.server.dao.sync.SyncEntityName; import org.avni.server.domain.*; import org.avni.server.domain.accessControl.PrivilegeType; @@ -28,9 +25,7 @@ import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRestartException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.*; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.hateoas.Link; import org.springframework.hateoas.PagedResources; @@ -58,9 +53,10 @@ public class SubjectMigrationController extends AbstractController scopeBasedSyncService, SubjectMigrationService subjectMigrationService, IndividualRepository individualRepository, LocationRepository locationRepository, AccessControlService accessControlService, Job bulkSubjectMigrationJob, JobLauncher bulkSubjectMigrationJobLauncher) { + public SubjectMigrationController(SubjectMigrationRepository subjectMigrationRepository, SubjectTypeRepository subjectTypeRepository, UserService userService, ScopeBasedSyncService scopeBasedSyncService, SubjectMigrationService subjectMigrationService, IndividualRepository individualRepository, LocationRepository locationRepository, AccessControlService accessControlService, Job bulkSubjectMigrationJob, JobLauncher bulkSubjectMigrationJobLauncher, AvniJobRepository avniJobRepository) { this.scopeBasedSyncService = scopeBasedSyncService; this.subjectMigrationService = subjectMigrationService; this.individualRepository = individualRepository; @@ -72,6 +68,7 @@ public SubjectMigrationController(SubjectMigrationRepository subjectMigrationRep this.subjectMigrationRepository = subjectMigrationRepository; this.subjectTypeRepository = subjectTypeRepository; this.userService = userService; + this.avniJobRepository = avniJobRepository; } @RequestMapping(value = "/subjectMigrations/v2", method = RequestMethod.GET) @@ -121,6 +118,9 @@ public Resource process(Resource resource) { public ResponseEntity migrate(@RequestParam(value = "mode", defaultValue = "byAddress") SubjectMigrationService.BulkSubjectMigrationModes mode, @RequestBody SubjectMigrationRequest subjectMigrationRequest) { accessControlService.checkPrivilege(PrivilegeType.MultiTxEntityTypeUpdate); + if (subjectMigrationRequest.getSubjectIds() == null) { + throw new BadRequestError("subjectIds is required"); + } if (mode == SubjectMigrationService.BulkSubjectMigrationModes.byAddress && subjectMigrationRequest.getDestinationAddresses() == null) { throw new BadRequestError("destinationAddresses is required for mode: byAddress"); } @@ -150,4 +150,13 @@ public ResponseEntity migrate(@RequestParam(value = "mode", defaultValue = "byAd return ResponseEntity.status(HttpStatus.ACCEPTED).body(jobUUID); } + + @RequestMapping(value = "/api/subjectMigration/bulk/status/{jobUuid}", method = RequestMethod.GET) + @PreAuthorize(value = "hasAnyAuthority('user')") + public JobStatus migrationStatus(@PathVariable("jobUuid") String jobUuid) { + accessControlService.checkPrivilege(PrivilegeType.MultiTxEntityTypeUpdate); + String jobFilterCondition = " and uuid = '" + jobUuid + "'"; + Page jobStatuses = avniJobRepository.getJobStatuses(UserContextHolder.getUser(), jobFilterCondition, PageRequest.of(0, 1)); + return jobStatuses != null ? jobStatuses.getContent().get(0) : null; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java index ec763045b..e90372887 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java @@ -5,16 +5,16 @@ import java.util.Map; public class SubjectMigrationRequest implements Serializable { - private List subjectUuids; + private List subjectIds; private Map destinationAddresses; private Map destinationSyncConcepts; - public List getSubjectUuids() { - return subjectUuids; + public List getSubjectIds() { + return subjectIds; } - public void setSubjectUuids(List subjectUuids) { - this.subjectUuids = subjectUuids; + public void setSubjectIds(List subjectIds) { + this.subjectIds = subjectIds; } public Map getDestinationAddresses() { diff --git a/avni-server-api/src/main/resources/api/external-api.yaml b/avni-server-api/src/main/resources/api/external-api.yaml index fda859ae6..c20290554 100644 --- a/avni-server-api/src/main/resources/api/external-api.yaml +++ b/avni-server-api/src/main/resources/api/external-api.yaml @@ -1203,7 +1203,7 @@ paths: post: tags: - Subject, Subject Migration, Program Encounter, Program Enrolment, Encounter, Group Subject - summary: Migrate subjects and its descendant data (like encounter etc) from one location to another, belonging to specified subject types. If you have a lot of addresses then the request may timeout, but the server will continue to process the API. Each source to destination mapping for each subject type, will be done in its own transaction. So for the example request given there will be 6 transactions (3 address mapping multiplied by 2 subject types). + summary: Migrate specified subjects and its descendant data (like encounter etc) from one location to another or from one sync concept(s) to another. Processing happens as a batch job. parameters: - name: auth-token in: header @@ -1221,6 +1221,15 @@ paths: schema: type: string default: 1 + - name: mode + in: query + description: "Mode for the migration" + required: false + explode: false + schema: + type: string + enum: ["byAddress", "bySyncConcept"] + default: "byAddress" requestBody: content: application/json: @@ -1233,15 +1242,60 @@ paths: "334657": "335043", "331106": "331466" }, - "subjectTypeIds": [ - 672, - 671 + "destinationSyncConcepts": { + "e5c37dc0-7660-49c2-abf1-87a7d2b3c5ef": "Maharashtra", + "5ecbc89b-3d06-4911-95c3-8227e9f78790": "Karnataka" + }, + "subjectIds": [ + "c47cc708-5707-42ee-8f27-098a567c3229", + "329760fe-aa7e-448a-9b48-2a3aaba7ffa5" ] } required: true responses: "200": description: successful + /api/subjectMigration/bulk/status/{jobId}: + get: + tags: + - Subject, Subject Migration + summary: Get status of a bulk subject migration job + description: | + Provide the ID of the job for which status needs to be checked. The ID is in UUID format. + parameters: + - name: jobId + in: path + required: true + style: simple + explode: false + schema: + type: string + format: uuid + - name: auth-token + in: header + required: false + style: simple + explode: false + schema: + type: string + description: token provided by cognito/keycloak + - name: version + in: query + description: "Version of the API to be called" + required: false + explode: false + schema: + type: string + default: 1 + responses: + "200": + description: successful + content: + application/json: + schema: + $ref: '#/components/schemas/JobStatusBody' + "400": + description: bad input parameter /api/subjectTree: delete: tags: @@ -1875,12 +1929,36 @@ components: properties: destinationAddresses: type: object - description: "as key value pairs. key is string and value is of type string. key is source address id and value is destination address id" - subjectTypeIds: + description: "Required if mode is byAddress. As key value pairs. key is string and value is of type string. key is source address id and value is destination address id" + destinationSyncConcepts: + type: object + description: "Required if mode is bySyncConcept. As key value pairs. key is string and value is of type string. key is sync concept uuid and value is destination value" + subjectIds: type: array - description: "array of numeric values. each item in array is the id of subject type" + description: "Required. array of long values. each item in array is the id of a subject to be migrated." items: - type: number + type: integer + JobStatusBody: + type: object + properties: + uuid: + type: string + description: "uuid of the job" + status: + type: string + description: "job status" + exitStatus: + type: string + description: "job exit status" + createTime: + type: string + description: "job create time" + startTime: + type: string + description: "job start time" + endTime: + type: string + description: "job end time" VoidSubjectCriteriaBody: type: object properties: From aa2c046aa1a4c8f32932f90d9e8e1d149a382a22 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 2 Aug 2024 14:42:25 +0530 Subject: [PATCH 051/129] avniproject/avni-webapp#1293 - throw UUID of the bundle item in the message so that it is easy to identify which record has failed during bundle upload. --- .../messaging/service/MessagingService.java | 24 +++-- .../server/common/BulkItemSaveException.java | 7 ++ .../batch/zip/BundleZipFileImporter.java | 88 +++++-------------- .../org/avni/server/service/CardService.java | 11 +++ .../service/ChecklistDetailService.java | 11 +++ .../avni/server/service/DashboardService.java | 11 +++ .../server/service/DocumentationService.java | 11 +++ .../server/service/EncounterTypeService.java | 22 +++++ .../service/EntityTypeRetrieverService.java | 6 +- .../server/service/GroupDashboardService.java | 4 +- .../avni/server/service/GroupRoleService.java | 19 +++- .../avni/server/service/GroupsService.java | 11 +++ .../service/IdentifierSourceService.java | 11 +++ .../service/IndividualRelationService.java | 11 +++ .../IndividualRelationshipTypeService.java | 12 ++- .../avni/server/service/LocationService.java | 23 +++-- .../avni/server/service/ProgramService.java | 22 +++++ .../server/service/SubjectTypeService.java | 27 +++++- .../server/service/TaskStatusService.java | 10 +++ .../avni/server/service/TaskTypeService.java | 11 +++ .../accessControl/GroupPrivilegeService.java | 53 ++++++----- .../service/application/MenuItemService.java | 14 +++ .../server/web/contract/ProgramContract.java | 7 ++ .../avni/server/web/request/CHSRequest.java | 17 +++- .../request/GroupPrivilegeContractWeb.java | 1 - .../request/IndividualRelationContract.java | 7 ++ .../webapp/IdentifierSourceContractWeb.java | 7 ++ .../service/MessagingServiceTest.java | 6 +- 28 files changed, 349 insertions(+), 115 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/common/BulkItemSaveException.java diff --git a/avni-server-api/src/main/java/org/avni/messaging/service/MessagingService.java b/avni-server-api/src/main/java/org/avni/messaging/service/MessagingService.java index 1bfbd57dd..a43b242c8 100644 --- a/avni-server-api/src/main/java/org/avni/messaging/service/MessagingService.java +++ b/avni-server-api/src/main/java/org/avni/messaging/service/MessagingService.java @@ -1,6 +1,7 @@ package org.avni.messaging.service; import com.bugsnag.Bugsnag; +import org.avni.messaging.contract.MessageRuleContract; import org.avni.messaging.domain.*; import org.avni.messaging.domain.exception.GlificGroupMessageFailureException; import org.avni.messaging.domain.exception.GlificNotConfiguredException; @@ -10,6 +11,7 @@ import org.avni.server.domain.RuleExecutionException; import org.avni.server.domain.User; import org.avni.server.framework.security.UserContextHolder; +import org.avni.server.service.EntityTypeRetrieverService; import org.avni.server.service.RuleService; import org.avni.server.web.request.rules.response.ScheduleRuleResponseEntity; import org.joda.time.DateTime; @@ -34,12 +36,13 @@ public class MessagingService { private final MessageReceiverService messageReceiverService; private final MessageRequestService messageRequestService; private final RuleService ruleService; - private MessageRequestQueueRepository messageRequestQueueRepository; - private ManualMessageRepository manualMessageRepository; - private GroupMessagingService groupMessagingService; - private Bugsnag bugsnag; + private final MessageRequestQueueRepository messageRequestQueueRepository; + private final ManualMessageRepository manualMessageRepository; + private final GroupMessagingService groupMessagingService; + private final Bugsnag bugsnag; - private IndividualMessagingService individualMessagingService; + private final IndividualMessagingService individualMessagingService; + private final EntityTypeRetrieverService entityTypeRetrieverService; @Autowired public MessagingService(MessageRuleRepository messageRuleRepository, MessageReceiverService messageReceiverService, @@ -47,7 +50,7 @@ public MessagingService(MessageRuleRepository messageRuleRepository, MessageRece MessageRequestQueueRepository messageRequestQueueRepository, ManualMessageRepository manualMessageRepository, RuleService ruleService, GroupMessagingService groupMessagingService, - IndividualMessagingService individualMessagingService, Bugsnag bugsnag) { + IndividualMessagingService individualMessagingService, Bugsnag bugsnag, EntityTypeRetrieverService entityTypeRetrieverService) { this.messageRuleRepository = messageRuleRepository; this.messageReceiverService = messageReceiverService; this.messageRequestService = messageRequestService; @@ -57,6 +60,7 @@ public MessagingService(MessageRuleRepository messageRuleRepository, MessageRece this.groupMessagingService = groupMessagingService; this.bugsnag = bugsnag; this.individualMessagingService = individualMessagingService; + this.entityTypeRetrieverService = entityTypeRetrieverService; } public MessageRule find(Long id) { @@ -182,4 +186,12 @@ private void sendManualMessage(MessageRequest messageRequest) throws PhoneNumber else individualMessagingService.sendManualMessage(messageRequest); } + + public void saveRules(MessageRuleContract[] messageRuleContracts) { + for (MessageRuleContract messageRuleContract : messageRuleContracts) { + MessageRule messageRule = this.find(messageRuleContract.getUuid()); + messageRule = MessageRuleContract.toModel(messageRuleContract, messageRule, entityTypeRetrieverService); + this.saveRule(messageRule); + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/common/BulkItemSaveException.java b/avni-server-api/src/main/java/org/avni/server/common/BulkItemSaveException.java new file mode 100644 index 000000000..4abde3593 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/common/BulkItemSaveException.java @@ -0,0 +1,7 @@ +package org.avni.server.common; + +public class BulkItemSaveException extends RuntimeException { + public BulkItemSaveException(Object contract, Exception exception) { + super(String.format("%s. %s.", exception.getMessage(), contract.toString()), exception); + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java index a532ff590..4560368f8 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java @@ -264,9 +264,7 @@ private void deployFile(String fileName, String fileData, List getStandardReportCardInputEncounterTypes(ReportCard c public boolean isNonScopeEntityChanged(DateTime lastModifiedDateTime) { return cardRepository.existsByLastModifiedDateTimeGreaterThan(lastModifiedDateTime); } + + public void saveCards(ReportCardBundleRequest[] cardContracts) { + for (ReportCardBundleRequest cardContract : cardContracts) { + try { + uploadCard(cardContract); + } catch (Exception e) { + throw new BulkItemSaveException(cardContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/ChecklistDetailService.java b/avni-server-api/src/main/java/org/avni/server/service/ChecklistDetailService.java index 07f3270ae..b645563b6 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ChecklistDetailService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ChecklistDetailService.java @@ -1,6 +1,7 @@ package org.avni.server.service; import org.avni.server.builder.ChecklistDetailBuilder; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.ChecklistDetailRepository; import org.avni.server.dao.ChecklistItemDetailRepository; import org.avni.server.domain.ChecklistDetail; @@ -75,4 +76,14 @@ public boolean isNonScopeEntityChanged(DateTime lastModifiedDateTime) { public List getAll() { return checklistDetailRepository.findAllByIsVoidedFalse(); } + + public void saveChecklists(ChecklistDetailRequest[] checklistDetailRequests) { + for (ChecklistDetailRequest checklistDetailRequest : checklistDetailRequests) { + try { + saveChecklist(checklistDetailRequest); + } catch (Exception e) { + throw new BulkItemSaveException(checklistDetailRequest, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java index ae6779634..f7a075ca9 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java @@ -1,5 +1,6 @@ package org.avni.server.service; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.*; import org.avni.server.domain.*; import org.avni.server.domain.app.dashboard.DashboardFilter; @@ -198,4 +199,14 @@ private void assertNoExistingDashboardWithName(String name) { public boolean isNonScopeEntityChanged(DateTime lastModifiedDateTime) { return dashboardRepository.existsByLastModifiedDateTimeGreaterThan(lastModifiedDateTime); } + + public void saveDashboards(DashboardBundleContract[] dashboardContracts) { + for (DashboardBundleContract dashboardContract : dashboardContracts) { + try { + uploadDashboard(dashboardContract); + } catch (Exception e) { + throw new BulkItemSaveException(dashboardContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/DocumentationService.java b/avni-server-api/src/main/java/org/avni/server/service/DocumentationService.java index 272de4ee1..8dd5634b7 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/DocumentationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/DocumentationService.java @@ -1,5 +1,6 @@ package org.avni.server.service; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.common.EntityHelper; import org.avni.server.dao.DocumentationRepository; import org.avni.server.domain.CHSEntity; @@ -87,4 +88,14 @@ private void assignUUIDIfNull(CHSEntity chsEntity, CHSRequest request) { chsEntity.setUuid(request.getUuid()); } } + + public void saveDocumentations(DocumentationContract[] documentationContracts) { + for (DocumentationContract documentationContract : documentationContracts) { + try { + saveDocumentation(documentationContract); + } catch (Exception e) { + throw new BulkItemSaveException(documentationContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/EncounterTypeService.java b/avni-server-api/src/main/java/org/avni/server/service/EncounterTypeService.java index ef621cc9f..4a0a5f9b9 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/EncounterTypeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/EncounterTypeService.java @@ -1,6 +1,7 @@ package org.avni.server.service; import org.avni.server.application.FormMapping; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.EncounterTypeRepository; import org.avni.server.dao.OperationalEncounterTypeRepository; import org.avni.server.dao.application.FormMappingRepository; @@ -9,6 +10,7 @@ import org.avni.server.domain.Organisation; import org.avni.server.web.request.EntityTypeContract; import org.avni.server.web.request.OperationalEncounterTypeContract; +import org.avni.server.web.request.OperationalEncounterTypesContract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -84,4 +86,24 @@ public Stream getAllGeneralEncounter() { public Stream getAllProgramEncounter() { return formMappingRepository.findByProgramNotNullAndEncounterTypeNotNullAndIsVoidedFalse().stream().map(FormMapping::getEncounterType); } + + public void saveEncounterTypes(EntityTypeContract[] entityTypeContracts) { + for (EntityTypeContract entityTypeContract : entityTypeContracts) { + try { + this.createEncounterType(entityTypeContract); + } catch (Exception e) { + throw new BulkItemSaveException(entityTypeContract, e); + } + } + } + + public void saveOperationalEncounterTypes(OperationalEncounterTypesContract operationalEncounterTypesContract, Organisation organisation) { + for (OperationalEncounterTypeContract operationalEncounterTypeContract : operationalEncounterTypesContract.getOperationalEncounterTypes()) { + try { + this.createOperationalEncounterType(operationalEncounterTypeContract, organisation); + } catch (Exception e) { + throw new BulkItemSaveException(operationalEncounterTypeContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/EntityTypeRetrieverService.java b/avni-server-api/src/main/java/org/avni/server/service/EntityTypeRetrieverService.java index ffc339789..02f1c548c 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/EntityTypeRetrieverService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/EntityTypeRetrieverService.java @@ -8,9 +8,9 @@ @Service public class EntityTypeRetrieverService { - private SubjectTypeRepository subjectTypeRepository; - private ProgramRepository programRepository; - private EncounterTypeRepository encounterTypeRepository; + private final SubjectTypeRepository subjectTypeRepository; + private final ProgramRepository programRepository; + private final EncounterTypeRepository encounterTypeRepository; @Autowired public EntityTypeRetrieverService(SubjectTypeRepository subjectTypeRepository, ProgramRepository programRepository, EncounterTypeRepository encounterTypeRepository) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java index 1052bb7bc..b699cd2f0 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java @@ -55,12 +55,12 @@ public void saveFromBundle(List request) { Long organisationId = UserContextHolder.getUserContext().getOrganisationId(); GroupDashboard groupDashboard = EntityHelper.newOrExistingEntity(groupDashboardRepository, contract.getUuid(), null, new GroupDashboard()); Group group = null; - if(contract.isGroupOneOfTheDefaultGroups() && !StringUtils.isEmpty(contract.getGroupName())) { + if (contract.isGroupOneOfTheDefaultGroups() && !StringUtils.isEmpty(contract.getGroupName())) { group = groupRepository.findByNameAndOrganisationId(contract.getGroupName(), organisationId); } else { group = groupRepository.findByUuid(contract.getGroupUUID()); } - if(group == null) { + if (group == null) { throw new RuntimeException("Unable to process import of Group Dashboards, due to missing mandatory details." + "\nPlease download a newer version of the bundle from the source organisation and try uploading again."); } diff --git a/avni-server-api/src/main/java/org/avni/server/service/GroupRoleService.java b/avni-server-api/src/main/java/org/avni/server/service/GroupRoleService.java index ef8674f65..9fe0d284a 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/GroupRoleService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/GroupRoleService.java @@ -1,7 +1,10 @@ package org.avni.server.service; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.GroupRoleRepository; +import org.avni.server.dao.SubjectTypeRepository; import org.avni.server.domain.GroupRole; +import org.avni.server.domain.Organisation; import org.avni.server.domain.SubjectType; import org.avni.server.web.request.GroupRoleContract; import org.slf4j.Logger; @@ -16,10 +19,12 @@ public class GroupRoleService implements NonScopeAwareService { private final GroupRoleRepository groupRoleRepository; private final Logger logger; + private final SubjectTypeRepository subjectTypeRepository; @Autowired - public GroupRoleService(GroupRoleRepository groupRoleRepository) { + public GroupRoleService(GroupRoleRepository groupRoleRepository, SubjectTypeRepository subjectTypeRepository) { this.groupRoleRepository = groupRoleRepository; + this.subjectTypeRepository = subjectTypeRepository; logger = LoggerFactory.getLogger(this.getClass()); } @@ -44,4 +49,16 @@ public GroupRole saveGroupRole(GroupRoleContract groupRoleRequest, SubjectType g public boolean isNonScopeEntityChanged(DateTime lastModifiedDateTime) { return groupRoleRepository.existsByLastModifiedDateTimeGreaterThan(lastModifiedDateTime); } + + public void saveGroupRoles(GroupRoleContract[] groupRoleContracts, Organisation organisation) { + for (GroupRoleContract groupRoleContract : groupRoleContracts) { + try { + SubjectType groupSubjectType = subjectTypeRepository.findByUuid(groupRoleContract.getGroupSubjectTypeUUID()); + SubjectType memberSubjectType = subjectTypeRepository.findByUuid(groupRoleContract.getMemberSubjectTypeUUID()); + this.saveGroupRole(groupRoleContract, groupSubjectType, memberSubjectType); + } catch (Exception e) { + throw new BulkItemSaveException(groupRoleContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/GroupsService.java b/avni-server-api/src/main/java/org/avni/server/service/GroupsService.java index 743aa651f..00c797d98 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/GroupsService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/GroupsService.java @@ -1,5 +1,6 @@ package org.avni.server.service; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.GroupRepository; import org.avni.server.domain.Group; import org.avni.server.domain.Organisation; @@ -40,4 +41,14 @@ public Group saveGroup(GroupContract groupContract, Organisation organisation) { public boolean isNonScopeEntityChanged(DateTime lastModifiedDateTime) { return groupRepository.existsByLastModifiedDateTimeGreaterThan(lastModifiedDateTime); } + + public void saveGroups(GroupContract[] groupContracts, Organisation organisation) { + for (GroupContract groupContract : groupContracts) { + try { + saveGroup(groupContract, organisation); + } catch (Exception e) { + throw new BulkItemSaveException(groupContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/IdentifierSourceService.java b/avni-server-api/src/main/java/org/avni/server/service/IdentifierSourceService.java index c3a1ce691..1f0966ce3 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IdentifierSourceService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IdentifierSourceService.java @@ -1,5 +1,6 @@ package org.avni.server.service; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.CatchmentRepository; import org.avni.server.dao.IdentifierSourceRepository; import org.avni.server.domain.CHSEntity; @@ -62,4 +63,14 @@ private Catchment getCatchment(Long catchmentId, String catchmentUUID) { public boolean isNonScopeEntityChanged(DateTime lastModifiedDateTime) { return identifierSourceRepository.existsByLastModifiedDateTimeGreaterThan(CHSEntity.toDate(lastModifiedDateTime)); } + + public void saveIdSources(IdentifierSourceContractWeb[] identifierSourceContractWebs) { + for (IdentifierSourceContractWeb identifierSourceContractWeb : identifierSourceContractWebs) { + try { + saveIdSource(identifierSourceContractWeb); + } catch (Exception e) { + throw new BulkItemSaveException(identifierSourceContractWeb, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationService.java b/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationService.java index 50529ea92..08fb794a5 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationService.java @@ -1,5 +1,6 @@ package org.avni.server.service; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.GenderRepository; import org.avni.server.dao.individualRelationship.IndividualRelationGenderMappingRepository; import org.avni.server.dao.individualRelationship.IndividualRelationRepository; @@ -149,4 +150,14 @@ public void deleteRelation(Long id) { public boolean isNonScopeEntityChanged(DateTime lastModifiedDateTime) { return individualRelationRepository.existsByLastModifiedDateTimeGreaterThan(lastModifiedDateTime); } + + public void saveRelations(IndividualRelationContract[] individualRelationContracts) { + for (IndividualRelationContract individualRelationContract : individualRelationContracts) { + try { + uploadRelation(individualRelationContract); + } catch (Exception e) { + throw new BulkItemSaveException(individualRelationContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationshipTypeService.java b/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationshipTypeService.java index 2052eba5a..b16e1dc00 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationshipTypeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IndividualRelationshipTypeService.java @@ -1,5 +1,6 @@ package org.avni.server.service; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.individualRelationship.IndividualRelationRepository; import org.avni.server.dao.individualRelationship.IndividualRelationshipTypeRepository; import org.avni.server.domain.individualRelationship.IndividualRelation; @@ -11,7 +12,6 @@ import org.joda.time.DateTime; import java.util.List; -import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; @@ -71,4 +71,14 @@ private IndividualRelationshipType createIndividualRelationshipType(IndividualRe public boolean isNonScopeEntityChanged(DateTime lastModifiedDateTime) { return individualRelationshipTypeRepository.existsByLastModifiedDateTimeGreaterThan(lastModifiedDateTime); } + + public void saveRelationshipTypes(IndividualRelationshipTypeContract[] individualRelationshipTypeContracts) { + for (IndividualRelationshipTypeContract individualRelationshipTypeContract : individualRelationshipTypeContracts) { + try { + saveRelationshipType(individualRelationshipTypeContract); + } catch (Exception e) { + throw new BulkItemSaveException(individualRelationshipTypeContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/LocationService.java b/avni-server-api/src/main/java/org/avni/server/service/LocationService.java index f10c8f9d9..c511a5f55 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/LocationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/LocationService.java @@ -6,6 +6,7 @@ import org.avni.server.application.projections.LocationProjection; import org.avni.server.builder.BuilderException; import org.avni.server.builder.LocationBuilder; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.*; import org.avni.server.dao.sync.SyncEntityName; import org.avni.server.domain.*; @@ -55,7 +56,9 @@ public LocationService(LocationRepository locationRepository, AddressLevelTypeRe public List saveAll(List locationContracts) throws BuilderException { List saved = new ArrayList<>(); - for (LocationContract contract : locationContracts) saved.add(save(contract)); + for (LocationContract contract : locationContracts) { + saved.add(save(contract)); + } return saved; } @@ -103,20 +106,20 @@ private AddressLevel saveLocation(LocationContract contract, AddressLevelType ty // Validate location title/name is unique only if new AddressLevel if (location.getId() == null && !titleIsValid(location, contract.getName().trim(), type)) - throw new BuilderException(String.format("Location with same name '%s' and type '%s' exists at this level", contract.getName(), type.getName())); + throw new BuilderException(String.format("Location with same name '%s' and type '%s' exists at this level. (%s)", contract.getName(), type.getName(), contract)); try { locationRepository.save(location); } catch (Exception e) { logger.error(e.getMessage()); - throw new BuilderException(String.format("Unable to create Location{name='%s',level='%s',orgUUID='%s',..}: '%s'", contract.getName(), contract.getLevel(), contract.getOrganisationUUID(), e.getMessage())); + throw new BuilderException(String.format("Unable to create Location{name='%s',level='%s',orgUUID='%s',..}: '%s' (%s)", contract.getName(), contract.getLevel(), contract.getOrganisationUUID(), e.getMessage(), contract)); } try { location.calculateLineage(); locationRepository.save(location); } catch (Exception e) { logger.error(e.getMessage()); - throw new BuilderException(String.format("Unable to update lineage for location with Id %s - %s", location.getId(), e.getMessage())); + throw new BuilderException(String.format("Unable to update lineage for location with Id %s - %s. (%s)", location.getId(), e.getMessage(), contract)); } return location; } @@ -305,6 +308,16 @@ public Page find(LocationSearchRequest searchRequest) { public List getParents(String uuid, Long maxLevelTypeId) { return maxLevelTypeId == null ? - locationRepository.getParents(uuid) : locationRepository.getParentsWithMaxLevelTypeId(uuid, maxLevelTypeId); + locationRepository.getParents(uuid) : locationRepository.getParentsWithMaxLevelTypeId(uuid, maxLevelTypeId); + } + + public void createAddressLevelTypes(AddressLevelTypeContract[] addressLevelTypeContracts) { + for (AddressLevelTypeContract addressLevelTypeContract : addressLevelTypeContracts) { + try { + this.createAddressLevelType(addressLevelTypeContract); + } catch (Exception e) { + throw new BulkItemSaveException(addressLevelTypeContract, e); + } + } } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/ProgramService.java b/avni-server-api/src/main/java/org/avni/server/service/ProgramService.java index a8a7cfb7b..b2cc405ad 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ProgramService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ProgramService.java @@ -2,12 +2,14 @@ import org.avni.server.application.FormMapping; import org.avni.server.application.FormType; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.OperationalProgramRepository; import org.avni.server.dao.ProgramRepository; import org.avni.server.dao.application.FormMappingRepository; import org.avni.server.domain.*; import org.avni.server.web.contract.ProgramContract; import org.avni.server.web.request.OperationalProgramContract; +import org.avni.server.web.request.OperationalProgramsContract; import org.avni.server.web.request.ProgramRequest; import org.avni.server.web.request.rules.response.EligibilityRuleEntity; import org.avni.server.web.request.rules.response.EligibilityRuleResponseEntity; @@ -142,4 +144,24 @@ private List getEligibleProgramsFromInactivePrograms(Individual individ return eligiblePrograms; } + + public void savePrograms(ProgramRequest[] programRequests) { + for (ProgramRequest programRequest : programRequests) { + try { + this.saveProgram(programRequest); + } catch (Exception e) { + throw new BulkItemSaveException(programRequest, e); + } + } + } + + public void saveOperationalPrograms(OperationalProgramsContract operationalProgramsContract, Organisation organisation) { + for (OperationalProgramContract opc : operationalProgramsContract.getOperationalPrograms()) { + try { + this.createOperationalProgram(opc, organisation); + } catch (Exception e) { + throw new BulkItemSaveException(opc, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java b/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java index a5e21533e..fb00be98b 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java @@ -2,6 +2,7 @@ import org.avni.server.application.Subject; import org.avni.server.application.SubjectTypeSettingKey; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.AddressLevelTypeRepository; import org.avni.server.dao.AvniJobRepository; import org.avni.server.dao.OperationalSubjectTypeRepository; @@ -9,6 +10,7 @@ import org.avni.server.domain.*; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.web.request.OperationalSubjectTypeContract; +import org.avni.server.web.request.OperationalSubjectTypesContract; import org.avni.server.web.request.SubjectTypeContract; import org.avni.server.web.request.syncAttribute.UserSyncAttributeAssignmentRequest; import org.avni.server.web.request.webapp.SubjectTypeSetting; @@ -273,8 +275,31 @@ public AddressLevelTypes getRegistrableLocationTypes(SubjectType subjectType) { } } + @Transactional + public void saveSubjectTypes(SubjectTypeContract[] subjectTypeContracts) { + for (SubjectTypeContract subjectTypeContract : subjectTypeContracts) { + try { + SubjectTypeUpsertResponse response = this.saveSubjectType(subjectTypeContract); + if (response.isSubjectTypeNotPresentInDB() && Subject.valueOf(subjectTypeContract.getType()).equals(Subject.User)) { + this.launchUserSubjectTypeJob(response.getSubjectType()); + } + } catch (Exception e) { + throw new BulkItemSaveException(subjectTypeContract, e); + } + } + } + + public void saveOperationalSubjectTypes(OperationalSubjectTypesContract operationalSubjectTypesContract, Organisation organisation) { + for (OperationalSubjectTypeContract ostc : operationalSubjectTypesContract.getOperationalSubjectTypes()) { + try { + this.createOperationalSubjectType(ostc, organisation); + } catch (Exception e) { + throw new BulkItemSaveException(ostc, e); + } + } + } - public class SubjectTypeUpsertResponse { + public static class SubjectTypeUpsertResponse { boolean isSubjectTypeNotPresentInDB; SubjectType subjectType; diff --git a/avni-server-api/src/main/java/org/avni/server/service/TaskStatusService.java b/avni-server-api/src/main/java/org/avni/server/service/TaskStatusService.java index a05c33d70..a4ed44e1c 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/TaskStatusService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/TaskStatusService.java @@ -1,5 +1,6 @@ package org.avni.server.service; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.common.EntityHelper; import org.avni.server.dao.task.TaskStatusRepository; import org.avni.server.dao.task.TaskTypeRepository; @@ -62,4 +63,13 @@ private TaskStatus buildTaskStatus(TaskStatusContract request) { return taskStatus; } + public void saveTaskStatuses(TaskStatusContract[] taskStatusContracts) { + for (TaskStatusContract taskStatusContract : taskStatusContracts) { + try { + saveTaskStatus(taskStatusContract); + } catch (Exception e) { + throw new BulkItemSaveException(taskStatusContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/TaskTypeService.java b/avni-server-api/src/main/java/org/avni/server/service/TaskTypeService.java index 4126dd6cc..521d9fbf3 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/TaskTypeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/TaskTypeService.java @@ -1,5 +1,6 @@ package org.avni.server.service; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.common.EntityHelper; import org.avni.server.dao.task.TaskTypeRepository; import org.avni.server.domain.task.TaskType; @@ -44,4 +45,14 @@ public TaskType saveTaskType(TaskTypeContract request) { taskTypeRepository.save(taskType); return taskType; } + + public void saveTaskTypes(TaskTypeContract[] taskTypeContracts) { + for (TaskTypeContract taskTypeContract : taskTypeContracts) { + try { + saveTaskType(taskTypeContract); + } catch (Exception e) { + throw new BulkItemSaveException(taskTypeContract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java index 105a0edbd..4488a55f4 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java @@ -4,6 +4,7 @@ import org.avni.server.application.FormMapping; import org.avni.server.application.FormType; import org.avni.server.application.Subject; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.*; import org.avni.server.dao.application.FormMappingRepository; import org.avni.server.domain.*; @@ -184,31 +185,35 @@ public void savePrivileges(GroupPrivilegeContractWeb[] requests, Organisation or List groups = groupRepository.findAll(); Arrays.stream(requests).forEach(request -> { - Group targetedGroup = getGroup(request, organisation, groups); - GroupPrivilege groupPrivilege = groupPrivileges.stream().filter(gp -> - Objects.equals(targetedGroup.getUuid(), gp.getGroupUuid()) - && Objects.equals(request.getPrivilegeUUID(), gp.getPrivilegeUuid()) - && Objects.equals(request.getSubjectTypeUUID(), gp.getSubjectTypeUuid()) - && Objects.equals(request.getProgramUUID(), gp.getProgramUuid()) - && Objects.equals(request.getProgramEncounterTypeUUID(), gp.getProgramEncounterTypeUuid()) - && Objects.equals(request.getEncounterTypeUUID(), gp.getEncounterTypeUuid()) - && Objects.equals(request.getChecklistDetailUUID(), gp.getChecklistDetailUuid())) - .findAny().orElse(null); - if (groupPrivilege == null) { - groupPrivilege = new GroupPrivilege(); - //don't use uuid from request for bundle uploads since there could be records with matching uuid with older impl_version in db and unique org_uuid constraint is violated - groupPrivilege.assignUUID(); - groupPrivilege.setPrivilege(CollectionUtil.findByUuid(privileges, request.getPrivilegeUUID())); - groupPrivilege.setSubjectType(CollectionUtil.findByUuid(subjectTypes, request.getSubjectTypeUUID())); - groupPrivilege.setProgram(CollectionUtil.findByUuid(programs, request.getProgramUUID())); - groupPrivilege.setEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getEncounterTypeUUID())); - groupPrivilege.setProgramEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getProgramEncounterTypeUUID())); - groupPrivilege.setChecklistDetail(CollectionUtil.findByUuid(checklistDetails, request.getChecklistDetailUUID())); - groupPrivilege.setGroup(targetedGroup); - } + try { + Group targetedGroup = getGroup(request, organisation, groups); + GroupPrivilege groupPrivilege = groupPrivileges.stream().filter(gp -> + Objects.equals(targetedGroup.getUuid(), gp.getGroupUuid()) + && Objects.equals(request.getPrivilegeUUID(), gp.getPrivilegeUuid()) + && Objects.equals(request.getSubjectTypeUUID(), gp.getSubjectTypeUuid()) + && Objects.equals(request.getProgramUUID(), gp.getProgramUuid()) + && Objects.equals(request.getProgramEncounterTypeUUID(), gp.getProgramEncounterTypeUuid()) + && Objects.equals(request.getEncounterTypeUUID(), gp.getEncounterTypeUuid()) + && Objects.equals(request.getChecklistDetailUUID(), gp.getChecklistDetailUuid())) + .findAny().orElse(null); + if (groupPrivilege == null) { + groupPrivilege = new GroupPrivilege(); + //don't use uuid from request for bundle uploads since there could be records with matching uuid with older impl_version in db and unique org_uuid constraint is violated + groupPrivilege.assignUUID(); + groupPrivilege.setPrivilege(CollectionUtil.findByUuid(privileges, request.getPrivilegeUUID())); + groupPrivilege.setSubjectType(CollectionUtil.findByUuid(subjectTypes, request.getSubjectTypeUUID())); + groupPrivilege.setProgram(CollectionUtil.findByUuid(programs, request.getProgramUUID())); + groupPrivilege.setEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getEncounterTypeUUID())); + groupPrivilege.setProgramEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getProgramEncounterTypeUUID())); + groupPrivilege.setChecklistDetail(CollectionUtil.findByUuid(checklistDetails, request.getChecklistDetailUUID())); + groupPrivilege.setGroup(targetedGroup); + } - groupPrivilege.setAllow(request.isAllow()); - groupPrivilegeRepository.saveGroupPrivilege(groupPrivilege); + groupPrivilege.setAllow(request.isAllow()); + groupPrivilegeRepository.saveGroupPrivilege(groupPrivilege); + } catch (Exception e) { + throw new BulkItemSaveException(request, e); + } }); } diff --git a/avni-server-api/src/main/java/org/avni/server/service/application/MenuItemService.java b/avni-server-api/src/main/java/org/avni/server/service/application/MenuItemService.java index 8adbe616d..048263a4e 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/application/MenuItemService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/application/MenuItemService.java @@ -1,8 +1,10 @@ package org.avni.server.service.application; import org.avni.server.application.menu.MenuItem; +import org.avni.server.common.BulkItemSaveException; import org.avni.server.dao.application.MenuItemRepository; import org.avni.server.service.NonScopeAwareService; +import org.avni.server.web.request.application.menu.MenuItemContract; import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -43,4 +45,16 @@ public MenuItem find(String uuid) { public MenuItem find(Long id) { return menuItemRepository.findEntity(id); } + + @Transactional + public void saveMenuItems(MenuItemContract[] menuItemContracts) { + for (MenuItemContract contract : menuItemContracts) { + try { + MenuItem menuItem = this.find(contract.getUuid()); + this.save(MenuItemContract.toEntity(contract, menuItem)); + } catch (Exception e) { + throw new BulkItemSaveException(contract, e); + } + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/ProgramContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/ProgramContract.java index 4e12181d3..5a6893b7f 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/contract/ProgramContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/ProgramContract.java @@ -119,4 +119,11 @@ public static ProgramContract createBasic(Program program) { contract.setUuid(program.getUuid()); return contract; } + + @Override + public String toString() { + return "{" + + "uuid='" + uuid + '\'' + + '}'; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java index b233fcd9e..5abcafc7b 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java @@ -32,9 +32,13 @@ public CHSRequest(CHSEntity chsEntity) { this(chsEntity.getUuid(), chsEntity.getId(), chsEntity.isVoided()); } - public Long getId() { return id; } + public Long getId() { + return id; + } - public void setId(Long id) { this.id = id; } + public void setId(Long id) { + this.id = id; + } public String getUuid() { return uuid == null ? null : uuid.trim(); @@ -57,4 +61,13 @@ public boolean isVoided() { public void setVoided(boolean voided) { isVoided = voided; } + + @Override + public String toString() { + return "{" + + "uuid='" + uuid + '\'' + + ", id=" + id + + ", isVoided=" + isVoided + + '}'; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java b/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java index 9bba2d449..d6d7a14ed 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java @@ -3,7 +3,6 @@ import org.avni.server.domain.accessControl.GroupPrivilege; public class GroupPrivilegeContractWeb extends CHSRequest { - private String groupUUID; private String privilegeUUID; private String subjectTypeUUID; diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/IndividualRelationContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/IndividualRelationContract.java index b9b6a943e..4b9e38a96 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/IndividualRelationContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/IndividualRelationContract.java @@ -59,4 +59,11 @@ public boolean isVoided() { public void setVoided(boolean voided) { this.voided = voided; } + + @Override + public String toString() { + return "{" + + "uuid='" + uuid + '\'' + + '}'; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/webapp/IdentifierSourceContractWeb.java b/avni-server-api/src/main/java/org/avni/server/web/request/webapp/IdentifierSourceContractWeb.java index 9ff17994a..72d9292fa 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/webapp/IdentifierSourceContractWeb.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/webapp/IdentifierSourceContractWeb.java @@ -138,4 +138,11 @@ public Long getId() { public void setId(Long id) { this.id = id; } + + @Override + public String toString() { + return "{" + + "UUID='" + UUID + '\'' + + '}'; + } } diff --git a/avni-server-api/src/test/java/org/avni/messaging/service/MessagingServiceTest.java b/avni-server-api/src/test/java/org/avni/messaging/service/MessagingServiceTest.java index 57cbbfc9b..20ac4ba07 100644 --- a/avni-server-api/src/test/java/org/avni/messaging/service/MessagingServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/messaging/service/MessagingServiceTest.java @@ -11,6 +11,7 @@ import org.avni.server.domain.User; import org.avni.server.domain.UserContext; import org.avni.server.framework.security.UserContextHolder; +import org.avni.server.service.EntityTypeRetrieverService; import org.avni.server.service.RuleService; import org.avni.server.web.request.rules.response.ScheduleRuleResponseEntity; import org.joda.time.DateTime; @@ -60,6 +61,9 @@ public class MessagingServiceTest { @Mock private IndividualMessagingService individualMessagingService; + @Mock + private EntityTypeRetrieverService entityTypeRetrieverService; + @Captor ArgumentCaptor messageReceiver; @@ -73,7 +77,7 @@ public void setup() { initMocks(this); messagingService = new MessagingService(messageRuleRepository, messageReceiverService, messageRequestService, messageRequestQueueRepository, - manualMessageRepository, ruleService, groupMessagingService, individualMessagingService, null); + manualMessageRepository, ruleService, groupMessagingService, individualMessagingService, null, entityTypeRetrieverService); scheduledSinceDays = "4"; } From 4cab990335a3e69ba6253c68814438d4b3ef735f Mon Sep 17 00:00:00 2001 From: Joy A Date: Fri, 2 Aug 2024 14:48:09 +0530 Subject: [PATCH 052/129] #747 | /api/subjectMigration/bulk failures to s3 --- .../BulkSubjectMigrationJobListener.java | 38 ++++++++++++++++--- .../BulkSubjectMigrationTasklet.java | 21 +++------- .../server/service/BulkUploadS3Service.java | 4 ++ .../service/SubjectMigrationService.java | 16 ++++---- .../web/SubjectMigrationController.java | 19 ++++++---- ....java => BulkSubjectMigrationRequest.java} | 2 +- 6 files changed, 64 insertions(+), 36 deletions(-) rename avni-server-api/src/main/java/org/avni/server/web/request/{SubjectMigrationRequest.java => BulkSubjectMigrationRequest.java} (93%) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java index 702aeae0f..923d70c2f 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationJobListener.java @@ -1,7 +1,10 @@ package org.avni.server.importer.batch.sync.attributes.bulkmigration; +import com.fasterxml.jackson.databind.ObjectMapper; import org.avni.server.framework.security.AuthService; -import org.avni.server.web.request.SubjectMigrationRequest; +import org.avni.server.service.BulkUploadS3Service; +import org.avni.server.util.ObjectMapperSingleton; +import org.avni.server.web.request.BulkSubjectMigrationRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.batch.core.JobExecution; @@ -11,11 +14,18 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import static java.lang.String.format; + @Component @JobScope public class BulkSubjectMigrationJobListener extends JobExecutionListenerSupport { private static final Logger logger = LoggerFactory.getLogger(BulkSubjectMigrationJobListener.class); private final AuthService authService; + private final BulkUploadS3Service s3Service; @Value("#{jobParameters['uuid']}") private String uuid; @@ -29,12 +39,16 @@ public class BulkSubjectMigrationJobListener extends JobExecutionListenerSupport @Value("#{jobParameters['userId']}") private Long userId; + @Value("#{jobParameters['fileName']}") + private String fileName; + @Value("#{jobParameters['bulkSubjectMigrationParameters']}") - private SubjectMigrationRequest bulkSubjectMigrationParameters; + private BulkSubjectMigrationRequest bulkSubjectMigrationParameters; @Autowired - public BulkSubjectMigrationJobListener(AuthService authService) { + public BulkSubjectMigrationJobListener(AuthService authService, BulkUploadS3Service s3Service) { this.authService = authService; + this.s3Service = s3Service; } @Override @@ -45,7 +59,21 @@ public void beforeJob(JobExecution jobExecution) { @Override public void afterJob(JobExecution jobExecution) { - logger.info("Finished Bulk Subject Migration Job {} mode: {} exitStatus: {} createTime: {} startTime: {} endTime: {}", - uuid, mode, jobExecution.getExitStatus(), jobExecution.getCreateTime(), jobExecution.getStartTime(), jobExecution.getEndTime()); + Map failedMigrations = (Map) jobExecution.getExecutionContext().get("failedMigrations"); + logger.info("Finished Bulk Subject Migration Job {} mode: {} failedCount: {} exitStatus: {} waitTime: {}ms processingTime: {}ms fileName: {}", + uuid, mode, failedMigrations.size(), jobExecution.getExitStatus(), jobExecution.getStartTime().getTime() - jobExecution.getCreateTime().getTime(), jobExecution.getEndTime().getTime() - jobExecution.getStartTime().getTime(), fileName); + try { + writeFailuresToFileAndUploadToS3(failedMigrations); + } catch (Exception e) { + logger.error("Failed to write bulk subject migration failures to file and upload", e); + } + + } + + private void writeFailuresToFileAndUploadToS3(Map failedMigrations) throws IOException { + ObjectMapper objectMapper = ObjectMapperSingleton.getObjectMapper(); + File failedMigrationsFile = new File(format("%s/%s", System.getProperty("java.io.tmpdir"), fileName)); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(failedMigrationsFile, failedMigrations); + s3Service.uploadFile(failedMigrationsFile, fileName, "bulksubjectmigrations"); } } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java index 153c67a96..9ed16eb20 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/sync/attributes/bulkmigration/BulkSubjectMigrationTasklet.java @@ -1,10 +1,8 @@ package org.avni.server.importer.batch.sync.attributes.bulkmigration; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.avni.server.service.S3Service; +import org.avni.server.service.BulkUploadS3Service; import org.avni.server.service.SubjectMigrationService; -import org.avni.server.util.ObjectMapperSingleton; -import org.avni.server.web.request.SubjectMigrationRequest; +import org.avni.server.web.request.BulkSubjectMigrationRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.batch.core.StepContribution; @@ -16,7 +14,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.io.File; import java.util.Map; @Component @@ -24,7 +21,7 @@ public class BulkSubjectMigrationTasklet implements Tasklet { private static final Logger logger = LoggerFactory.getLogger(BulkSubjectMigrationTasklet.class); private final SubjectMigrationService subjectMigrationService; - private final S3Service s3Service; + @Value("#{jobParameters['uuid']}") String uuid; @@ -32,23 +29,17 @@ public class BulkSubjectMigrationTasklet implements Tasklet { String mode; @Value("#{jobParameters['bulkSubjectMigrationParameters']}") - SubjectMigrationRequest bulkSubjectMigrationParameters; + BulkSubjectMigrationRequest bulkSubjectMigrationParameters; @Autowired - public BulkSubjectMigrationTasklet(SubjectMigrationService subjectMigrationService, S3Service s3Service) { + public BulkSubjectMigrationTasklet(SubjectMigrationService subjectMigrationService, BulkUploadS3Service s3Service) { this.subjectMigrationService = subjectMigrationService; - this.s3Service = s3Service; } @Override public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception { Map failedMigrations = subjectMigrationService.bulkMigrate(SubjectMigrationService.BulkSubjectMigrationModes.valueOf(mode), bulkSubjectMigrationParameters); - ObjectMapper objectMapper = ObjectMapperSingleton.getObjectMapper(); - String fileName = uuid + ".json"; - File failedMigrationsFile = new File("/tmp/" + fileName); - objectMapper.writerWithDefaultPrettyPrinter().writeValue(failedMigrationsFile, failedMigrations); -// TODO upload file to S3 -// s3Service.uploadFile(failedMigrationsFile, fileName, "bulkuploads/subjectmigrations"); + chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext().put("failedMigrations", failedMigrations); return RepeatStatus.FINISHED; } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/BulkUploadS3Service.java b/avni-server-api/src/main/java/org/avni/server/service/BulkUploadS3Service.java index fb3e5b37c..95dc24e18 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/BulkUploadS3Service.java +++ b/avni-server-api/src/main/java/org/avni/server/service/BulkUploadS3Service.java @@ -37,6 +37,10 @@ public ObjectInfo uploadErrorFile(File tempSourceFile, String uuid) throws IOExc return s3Service.uploadFile(tempSourceFile, format("%s.csv", uuid), "bulkuploads/error"); } + public ObjectInfo uploadFile(File localFile, String filename, String directory) throws IOException { + return s3Service.uploadFile(localFile, filename, directory); + } + public File getLocalErrorFile(String uuid) { File errorDir = new File(format("%s/bulkuploads/error", System.getProperty("java.io.tmpdir"))); errorDir.mkdirs(); diff --git a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java index 59d41ba9a..c3be7f16b 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java @@ -8,7 +8,7 @@ import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.accessControl.AccessControlService; import org.avni.server.web.IndividualController; -import org.avni.server.web.request.SubjectMigrationRequest; +import org.avni.server.web.request.BulkSubjectMigrationRequest; import org.joda.time.DateTime; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -155,11 +155,11 @@ public void changeSubjectSyncConceptValues(Individual subject, String destinatio individualService.save(subject); } - public Map bulkMigrate(BulkSubjectMigrationModes mode, SubjectMigrationRequest subjectMigrationRequest) { + public Map bulkMigrate(BulkSubjectMigrationModes mode, BulkSubjectMigrationRequest bulkSubjectMigrationRequest) { if (mode == BulkSubjectMigrationModes.byAddress) { - return bulkMigrateByAddress(subjectMigrationRequest.getSubjectIds(), subjectMigrationRequest.getDestinationAddresses()); + return bulkMigrateByAddress(bulkSubjectMigrationRequest.getSubjectIds(), bulkSubjectMigrationRequest.getDestinationAddresses()); } else { - return bulkMigrateBySyncConcept(subjectMigrationRequest.getSubjectIds(), subjectMigrationRequest.getDestinationSyncConcepts()); + return bulkMigrateBySyncConcept(bulkSubjectMigrationRequest.getSubjectIds(), bulkSubjectMigrationRequest.getDestinationSyncConcepts()); } } @@ -219,18 +219,20 @@ public Map bulkMigrateBySyncConcept(List subjectIds, Map destinationSyncConcepts) { + String destinationSyncConceptValue = destinationSyncConcepts.get(subjectTypeSyncConceptUuid); + if (subjectTypeSyncConceptUuid != null && destinationSyncConceptValue == null) { + return null; // No migration required for this sync concept. + } if (subjectTypeSyncConceptUuid == null) { throw new RuntimeException("No sync concept configured for subject type"); } Concept syncConcept = conceptRepository.findByUuid(subjectTypeSyncConceptUuid); - String destinationSyncConceptValue = destinationSyncConcepts.get(subjectTypeSyncConceptUuid); - if (Objects.equals(currentValue, destinationSyncConceptValue)) { throw new RuntimeException("Source value and Destination value are the same"); } - if (destinationSyncConceptValue != null && syncConcept.isCoded()) { + if (syncConcept.isCoded()) { ConceptAnswer conceptAnswer = syncConcept.findConceptAnswerByConceptUUID(destinationSyncConceptValue); if (conceptAnswer == null || conceptAnswer.isVoided()) { throw new RuntimeException(String.format("Invalid value '%s' for coded sync concept", destinationSyncConceptValue)); diff --git a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java index 28501648d..73ac7928a 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java @@ -11,7 +11,7 @@ import org.avni.server.service.UserService; import org.avni.server.service.accessControl.AccessControlService; import org.avni.server.util.BadRequestError; -import org.avni.server.web.request.SubjectMigrationRequest; +import org.avni.server.web.request.BulkSubjectMigrationRequest; import org.avni.server.web.response.slice.SlicedResources; import org.joda.time.DateTime; import org.slf4j.Logger; @@ -38,6 +38,7 @@ import java.util.Collections; import java.util.UUID; +import static java.lang.String.format; import static org.avni.server.web.resourceProcessors.ResourceProcessor.addAuditFields; @RestController @@ -116,15 +117,15 @@ public Resource process(Resource resource) { @RequestMapping(value = "/api/subjectMigration/bulk", method = RequestMethod.POST) @PreAuthorize(value = "hasAnyAuthority('user')") public ResponseEntity migrate(@RequestParam(value = "mode", defaultValue = "byAddress") SubjectMigrationService.BulkSubjectMigrationModes mode, - @RequestBody SubjectMigrationRequest subjectMigrationRequest) { + @RequestBody BulkSubjectMigrationRequest bulkSubjectMigrationRequest) { accessControlService.checkPrivilege(PrivilegeType.MultiTxEntityTypeUpdate); - if (subjectMigrationRequest.getSubjectIds() == null) { + if (bulkSubjectMigrationRequest.getSubjectIds() == null) { throw new BadRequestError("subjectIds is required"); } - if (mode == SubjectMigrationService.BulkSubjectMigrationModes.byAddress && subjectMigrationRequest.getDestinationAddresses() == null) { + if (mode == SubjectMigrationService.BulkSubjectMigrationModes.byAddress && bulkSubjectMigrationRequest.getDestinationAddresses() == null) { throw new BadRequestError("destinationAddresses is required for mode: byAddress"); } - if (mode == SubjectMigrationService.BulkSubjectMigrationModes.bySyncConcept && subjectMigrationRequest.getDestinationSyncConcepts() == null) { + if (mode == SubjectMigrationService.BulkSubjectMigrationModes.bySyncConcept && bulkSubjectMigrationRequest.getDestinationSyncConcepts() == null) { throw new BadRequestError("destinationSyncConcepts is required for mode: bySyncConcepts"); } @@ -132,13 +133,15 @@ public ResponseEntity migrate(@RequestParam(value = "mode", defaultValue = "byAd User user = userContext.getUser(); Organisation organisation = userContext.getOrganisation(); String jobUUID = UUID.randomUUID().toString(); + String fileName = format("%s-%s-%s.%s", jobUUID, mode, user.getUsername(), "json"); JobParameters jobParameters = new JobParametersBuilder() .addString("uuid", jobUUID) .addString("organisationUUID", organisation.getUuid()) .addLong("userId", user.getId(), false) .addString("mode", String.valueOf(mode)) - .addParameter("bulkSubjectMigrationParameters", new CustomJobParameter<>(subjectMigrationRequest)) + .addString("fileName", fileName) + .addParameter("bulkSubjectMigrationParameters", new CustomJobParameter<>(bulkSubjectMigrationRequest)) .toJobParameters(); try { @@ -148,7 +151,7 @@ public ResponseEntity migrate(@RequestParam(value = "mode", defaultValue = "byAd throw new RuntimeException(String.format("Error while starting the bulk subject migration job, %s", e.getMessage()), e); } - return ResponseEntity.status(HttpStatus.ACCEPTED).body(jobUUID); + return ResponseEntity.status(HttpStatus.ACCEPTED).body(migrationStatus(jobUUID)); } @RequestMapping(value = "/api/subjectMigration/bulk/status/{jobUuid}", method = RequestMethod.GET) @@ -157,6 +160,6 @@ public JobStatus migrationStatus(@PathVariable("jobUuid") String jobUuid) { accessControlService.checkPrivilege(PrivilegeType.MultiTxEntityTypeUpdate); String jobFilterCondition = " and uuid = '" + jobUuid + "'"; Page jobStatuses = avniJobRepository.getJobStatuses(UserContextHolder.getUser(), jobFilterCondition, PageRequest.of(0, 1)); - return jobStatuses != null ? jobStatuses.getContent().get(0) : null; + return (jobStatuses != null && !jobStatuses.getContent().isEmpty()) ? jobStatuses.getContent().get(0) : null; } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/BulkSubjectMigrationRequest.java similarity index 93% rename from avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java rename to avni-server-api/src/main/java/org/avni/server/web/request/BulkSubjectMigrationRequest.java index e90372887..daa3a6b29 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/SubjectMigrationRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/BulkSubjectMigrationRequest.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Map; -public class SubjectMigrationRequest implements Serializable { +public class BulkSubjectMigrationRequest implements Serializable { private List subjectIds; private Map destinationAddresses; private Map destinationSyncConcepts; From 37efb57fd783c9f543caf324887fccd5d14b8630 Mon Sep 17 00:00:00 2001 From: Joy A Date: Fri, 2 Aug 2024 14:54:08 +0530 Subject: [PATCH 053/129] #747 | /api/subjectMigration/bulk update api doc --- .../src/main/resources/api/external-api.yaml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/avni-server-api/src/main/resources/api/external-api.yaml b/avni-server-api/src/main/resources/api/external-api.yaml index c20290554..94f5f960f 100644 --- a/avni-server-api/src/main/resources/api/external-api.yaml +++ b/avni-server-api/src/main/resources/api/external-api.yaml @@ -1253,8 +1253,14 @@ paths: } required: true responses: - "200": - description: successful + "202": + description: accepted + content: + application/json: + schema: + $ref: '#/components/schemas/JobStatusBody' + "400": + description: bad request /api/subjectMigration/bulk/status/{jobId}: get: tags: @@ -1959,6 +1965,9 @@ components: endTime: type: string description: "job end time" + fileName: + type: string + description: "Filename containing result of the migration process available on S3" VoidSubjectCriteriaBody: type: object properties: From 924217011fb7e12af63cc51aebe6bbb3f9523029 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 2 Aug 2024 15:07:25 +0530 Subject: [PATCH 054/129] avniproject/avni-webapp#1293 - put id only if available --- .../java/org/avni/server/web/request/CHSRequest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java index 5abcafc7b..27d98afe8 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java @@ -64,10 +64,11 @@ public void setVoided(boolean voided) { @Override public String toString() { - return "{" + - "uuid='" + uuid + '\'' + - ", id=" + id + - ", isVoided=" + isVoided + - '}'; + final StringBuilder sb = new StringBuilder("{"); + sb.append("uuid='").append(uuid).append('\''); + if (id != null && id != 0) + sb.append(", id=").append(id); + sb.append('}'); + return sb.toString(); } } From 914619585460cd2a0836e07c85b7cf4a5db6adb7 Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 2 Aug 2024 17:25:10 +0530 Subject: [PATCH 055/129] #768 | Add null check in get and rename var in void of identifierUserAssignment --- .../java/org/avni/server/builder/ChecklistDetailBuilder.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java index 09d6b38a8..1e90d6a9d 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java @@ -49,6 +49,7 @@ public ChecklistDetailBuilder withItems(List items) .withVoided(item.isVoided()) .withform(form) .withConcept(concept) + .withLeadItem(builtItems.get(item.getDependentOn())) .withScheduleOnExpiryOfDependency(item.getScheduleOnExpiryOfDependency()) .withMinDaysFromStartDate(item.getMinDaysFromStartDate()) .withMinDaysFromDependent(item.getMinDaysFromDependent()) @@ -56,8 +57,6 @@ public ChecklistDetailBuilder withItems(List items) .build(); builtItems.put(builtItemDetail.getUuid(), builtItemDetail); }); - //set dependentOn after all items in request have been processed so order of items in request does not matter for dependents - items.forEach(item -> getExistingChecklistItemDetail(this.get(), item).setLeadChecklistItemDetail(builtItems.get(item.getDependentOn()))); return this; } From e87b06b70e0888f0392c667b03cd30987b889046 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 2 Aug 2024 17:42:14 +0530 Subject: [PATCH 056/129] avniproject/avni-webapp#1298 - service method to check for whether the user can save a subject in a org partition. --- .../dao/VirtualCatchmentRepository.java | 2 + .../java/org/avni/server/domain/User.java | 8 +++ .../SubjectPartitionCheckStatus.java | 25 ++++++++ .../accessControl/SubjectPartitionData.java | 36 ++++++++++++ .../avni/server/service/CatchmentService.java | 11 +++- .../service/UserSubjectAssignmentService.java | 4 ++ .../accessControl/AccessControlService.java | 57 ++++++++++++++++--- .../org/avni/server/web/UserController.java | 2 +- .../syncAttribute/UserSyncSettings.java | 10 +++- .../AccessControlServiceStub.java | 2 +- .../AccessControlServiceTest.java | 4 +- 11 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java create mode 100644 avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionData.java diff --git a/avni-server-api/src/main/java/org/avni/server/dao/VirtualCatchmentRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/VirtualCatchmentRepository.java index 9ace1ea1f..a9a62c59a 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/VirtualCatchmentRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/VirtualCatchmentRepository.java @@ -1,5 +1,6 @@ package org.avni.server.dao; +import org.avni.server.domain.AddressLevel; import org.avni.server.domain.Catchment; import org.avni.server.domain.VirtualCatchment; import org.springframework.stereotype.Repository; @@ -9,4 +10,5 @@ @Repository public interface VirtualCatchmentRepository extends AvniJpaRepository { List findByCatchment(Catchment catchment); + boolean existsByAddressLevelAndCatchment(AddressLevel addressLevel, Catchment catchment); } diff --git a/avni-server-api/src/main/java/org/avni/server/domain/User.java b/avni-server-api/src/main/java/org/avni/server/domain/User.java index e2457445b..80ca4fa94 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/User.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/User.java @@ -1,7 +1,10 @@ package org.avni.server.domain; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.validator.routines.EmailValidator; +import org.avni.server.util.ObjectMapperSingleton; +import org.avni.server.web.request.syncAttribute.UserSyncSettings; import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.Type; @@ -106,6 +109,11 @@ public boolean hasAllPrivileges() { return getUserGroups().stream().anyMatch(userGroup -> userGroup.getGroup().isHasAllPrivileges()); } + public List getSyncSettingsList() { + User.SyncSettingKeys.subjectTypeSyncSettings.name(); + return ObjectMapperSingleton.getObjectMapper().convertValue(syncSettings.get(User.SyncSettingKeys.subjectTypeSyncSettings.name()), new TypeReference>() {}); + } + public enum SyncSettingKeys { syncAttribute1, syncAttribute2, diff --git a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java new file mode 100644 index 000000000..536dee909 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java @@ -0,0 +1,25 @@ +package org.avni.server.domain.accessControl; + +public class SubjectPartitionCheckStatus { + public static final String NotDirectlyAssignedToThisUser = "notDirectlyAssignedToThisUser"; + public static final String NotInThisUsersCatchment = "notInThisUsersCatchment"; + public static final String SubjectTypeNotConfigured = "subjectTypeNotConfigured"; + public static final String UserSyncAttributeNotConfigured = "userSyncAttributeNotConfigured"; + public static final String SyncAttributeForUserNotValidForUpdate = "syncAttributeForUserNotValidForUpdate"; + + private final boolean passed; + private final String message; + + private SubjectPartitionCheckStatus(boolean passed, String message) { + this.passed = passed; + this.message = message; + } + + public static SubjectPartitionCheckStatus passed() { + return new SubjectPartitionCheckStatus(true, null); + } + + public static SubjectPartitionCheckStatus failed(String message) { + return new SubjectPartitionCheckStatus(false, message); + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionData.java b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionData.java new file mode 100644 index 000000000..7ffeb481d --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionData.java @@ -0,0 +1,36 @@ +package org.avni.server.domain.accessControl; + +import org.avni.server.domain.AddressLevel; +import org.avni.server.domain.Individual; +import org.springframework.util.StringUtils; + +public class SubjectPartitionData { + private final AddressLevel addressLevel; + private final String sync1ConceptValue; + private final String sync2ConceptValue; + + public SubjectPartitionData(AddressLevel addressLevel, String sync1ConceptValue, String sync2ConceptValue) { + if (addressLevel == null && StringUtils.isEmpty(sync1ConceptValue)) { + throw new IllegalArgumentException("AddressLevel or sync1ConceptValue must be provided"); + } + this.addressLevel = addressLevel; + this.sync1ConceptValue = sync1ConceptValue; + this.sync2ConceptValue = sync2ConceptValue; + } + + public SubjectPartitionData(Individual subject) { + this(subject.getAddressLevel(), subject.getSyncConcept1Value(), subject.getSyncConcept2Value()); + } + + public AddressLevel getAddressLevel() { + return addressLevel; + } + + public String getSync1ConceptValue() { + return sync1ConceptValue; + } + + public String getSync2ConceptValue() { + return sync2ConceptValue; + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java b/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java index 699df0aeb..89fa44a71 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java @@ -3,6 +3,7 @@ import org.avni.server.builder.BuilderException; import org.avni.server.dao.CatchmentRepository; import org.avni.server.dao.LocationRepository; +import org.avni.server.dao.VirtualCatchmentRepository; import org.avni.server.domain.AddressLevel; import org.avni.server.domain.Catchment; import org.avni.server.domain.Organisation; @@ -26,15 +27,15 @@ @Service public class CatchmentService { private final CatchmentRepository catchmentRepository; - private final UserService userService; private final LocationRepository locationRepository; + private final VirtualCatchmentRepository virtualCatchmentRepository; private final Logger logger; @Autowired - public CatchmentService(CatchmentRepository catchmentRepository, UserService userService, LocationRepository locationRepository) { + public CatchmentService(CatchmentRepository catchmentRepository, LocationRepository locationRepository, VirtualCatchmentRepository virtualCatchmentRepository) { this.catchmentRepository = catchmentRepository; - this.userService = userService; this.locationRepository = locationRepository; + this.virtualCatchmentRepository = virtualCatchmentRepository; logger = LoggerFactory.getLogger(this.getClass()); } @@ -114,4 +115,8 @@ private boolean catchmentExistsWithSameNameAndDifferentUUID(CatchmentContract ca Catchment catchment = catchmentRepository.findByName(catchmentRequest.getName()); return catchment != null && !catchment.getUuid().equals(catchmentRequest.getUuid()); } + + public boolean hasLocation(AddressLevel addressLevel, Catchment catchment) { + return virtualCatchmentRepository.existsByAddressLevelAndCatchment(addressLevel, catchment); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserSubjectAssignmentService.java b/avni-server-api/src/main/java/org/avni/server/service/UserSubjectAssignmentService.java index c4e50b45f..319831c06 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserSubjectAssignmentService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserSubjectAssignmentService.java @@ -257,4 +257,8 @@ private void triggerSyncForSubjectAndItsChildrenForUser(Individual individual) { ) .forEach(CHSEntity::updateAudit); } + + public boolean isAssignedToUser(Individual subject, User user) { + return this.userSubjectAssignmentRepository.findByUserAndSubject(user, subject) != null; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java index 61e141872..286c64b70 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java @@ -2,10 +2,11 @@ import org.avni.server.dao.*; import org.avni.server.domain.*; -import org.avni.server.domain.accessControl.AvniAccessException; -import org.avni.server.domain.accessControl.AvniNoUserSessionException; -import org.avni.server.domain.accessControl.PrivilegeType; +import org.avni.server.domain.accessControl.*; import org.avni.server.framework.security.UserContextHolder; +import org.avni.server.service.CatchmentService; +import org.avni.server.service.UserSubjectAssignmentService; +import org.avni.server.web.request.syncAttribute.UserSyncSettings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -23,14 +24,18 @@ public class AccessControlService { private final ProgramRepository programRepository; private final EncounterTypeRepository encounterTypeRepository; private final PrivilegeRepository privilegeRepository; + private final CatchmentService catchmentService; + private final UserSubjectAssignmentService userSubjectAssignmentService; @Autowired - public AccessControlService(UserRepository userRepository, SubjectTypeRepository subjectTypeRepository, ProgramRepository programRepository, EncounterTypeRepository encounterTypeRepository, PrivilegeRepository privilegeRepository) { + public AccessControlService(UserRepository userRepository, SubjectTypeRepository subjectTypeRepository, ProgramRepository programRepository, EncounterTypeRepository encounterTypeRepository, PrivilegeRepository privilegeRepository, CatchmentService catchmentService, UserSubjectAssignmentService userSubjectAssignmentService) { this.userRepository = userRepository; this.subjectTypeRepository = subjectTypeRepository; this.programRepository = programRepository; this.encounterTypeRepository = encounterTypeRepository; this.privilegeRepository = privilegeRepository; + this.catchmentService = catchmentService; + this.userSubjectAssignmentService = userSubjectAssignmentService; } public void checkPrivilege(PrivilegeType privilegeType) { @@ -70,7 +75,7 @@ public void checkSubjectPrivilege(PrivilegeType privilegeType, List subj subjectTypeUUIDs.forEach(s -> this.checkSubjectPrivilege(UserContextHolder.getUser(), privilegeType, s)); } - public void checkSubjectPrivileges(PrivilegeType privilegeType, Individual ... subjects) { + public void checkSubjectPrivileges(PrivilegeType privilegeType, Individual... subjects) { this.checkSubjectPrivileges(privilegeType, Arrays.stream(subjects).collect(Collectors.toList())); } @@ -206,9 +211,47 @@ public void checkApprovePrivilegeOnEntityApprovalStatus(String entityType, Strin public void checkApprovePrivilegeOnEntityApprovalStatuses(List entityApprovalStatuses) { Map, Long> uniqueEASByTypeAndTypeUuid = entityApprovalStatuses - .stream() - .collect(Collectors.groupingBy(entityApprovalStatus -> Arrays.asList(String.valueOf(entityApprovalStatus.getEntityType()), entityApprovalStatus.getEntityTypeUuid()), Collectors.counting())); + .stream() + .collect(Collectors.groupingBy(entityApprovalStatus -> Arrays.asList(String.valueOf(entityApprovalStatus.getEntityType()), entityApprovalStatus.getEntityTypeUuid()), Collectors.counting())); uniqueEASByTypeAndTypeUuid.keySet().forEach(entity -> checkApprovePrivilegeOnEntityApprovalStatus(entity.get(0), entity.get(1))); } + + // Since an Individual can be saved multiple times in a single transaction, the flush can also happen in between, the method expects that the pre-save state is explicitly passed. + public SubjectPartitionCheckStatus checkSubjectAccess(Individual subject, User user, SubjectPartitionData previousPartitionState) { + boolean firstTimeCreation = previousPartitionState == null; + SubjectType subjectType = subject.getSubjectType(); + SubjectPartitionData applicablePartitionData = firstTimeCreation ? new SubjectPartitionData(subject) : previousPartitionState; + + if (subjectType.isShouldSyncByLocation() && !catchmentService.hasLocation(applicablePartitionData.getAddressLevel(), user.getCatchment())) { + return SubjectPartitionCheckStatus.failed(SubjectPartitionCheckStatus.NotInThisUsersCatchment); + } + + if (subjectType.isDirectlyAssignable() && !firstTimeCreation && !userSubjectAssignmentService.isAssignedToUser(subject, user)) { + return SubjectPartitionCheckStatus.failed(SubjectPartitionCheckStatus.NotDirectlyAssignedToThisUser); + } + + boolean checkByObservationValue = !subjectType.isShouldSyncByLocation() && !subjectType.isDirectlyAssignable(); + if (checkByObservationValue) { + if (subjectType.getSyncRegistrationConcept1() == null) { + return SubjectPartitionCheckStatus.failed(SubjectPartitionCheckStatus.SubjectTypeNotConfigured); + } + + List syncSettingsList = user.getSyncSettingsList(); + UserSyncSettings userSyncSettingsForSubjectType = syncSettingsList.stream().filter(userSyncSettings -> userSyncSettings.getSubjectTypeUUID().equals(subjectType.getUuid())).findFirst().orElse(null); + if (userSyncSettingsForSubjectType == null) { + return SubjectPartitionCheckStatus.failed(SubjectPartitionCheckStatus.UserSyncAttributeNotConfigured); + } + + if (subjectType.getSyncRegistrationConcept1() != null && !userSyncSettingsForSubjectType.hasSync1Value(applicablePartitionData.getSync1ConceptValue())) { + return SubjectPartitionCheckStatus.failed(SubjectPartitionCheckStatus.SyncAttributeForUserNotValidForUpdate); + } + + if (subjectType.getSyncRegistrationConcept2() != null && !userSyncSettingsForSubjectType.hasSync1Value(applicablePartitionData.getSync2ConceptValue())) { + return SubjectPartitionCheckStatus.failed(SubjectPartitionCheckStatus.SyncAttributeForUserNotValidForUpdate); + } + } + + return SubjectPartitionCheckStatus.passed(); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserController.java b/avni-server-api/src/main/java/org/avni/server/web/UserController.java index 04f554de8..038f547e0 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserController.java @@ -124,7 +124,7 @@ public UserContract getUser(@PathVariable("id") Long id) { throw new EntityNotFoundException(String.format("User not found with id %d", id)); } UserContract userContract = UserContract.fromEntity(user); - userContract.setSyncSettings(UserSyncSettings.fromUserSyncSettings(user.getSyncSettings(), subjectTypeRepository)); + userContract.setSyncSettings(UserSyncSettings.toWebResponse(user.getSyncSettings(), subjectTypeRepository)); return userContract; } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/syncAttribute/UserSyncSettings.java b/avni-server-api/src/main/java/org/avni/server/web/request/syncAttribute/UserSyncSettings.java index a7ffb16b4..e44dfe702 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/syncAttribute/UserSyncSettings.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/syncAttribute/UserSyncSettings.java @@ -56,7 +56,7 @@ private static boolean isValidAttribute(LinkedHashMap syncSettings, String keyNa return syncSettings.containsKey(keyName) && !S.isEmpty((String) syncSettings.get(keyName)); } - public static JsonObject fromUserSyncSettings(JsonObject syncSettings, SubjectTypeRepository subjectTypeRepository) { + public static JsonObject toWebResponse(JsonObject syncSettings, SubjectTypeRepository subjectTypeRepository) { JsonObject response = new JsonObject(); if (!syncSettings.containsKey(User.SyncSettingKeys.subjectTypeSyncSettings.name())) { return response; @@ -119,4 +119,12 @@ public void setSyncConcept2(String syncConcept2) { public void setSyncConcept2Values(List syncConcept2Values) { this.syncConcept2Values = syncConcept2Values; } + + public boolean hasSync1Value(String value) { + return this.getSyncConcept1Values().contains(value); + } + + public boolean hasSync2Value(String value) { + return this.getSyncConcept2Values().contains(value); + } } diff --git a/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceStub.java b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceStub.java index 9d5adedfd..33a2602fb 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceStub.java +++ b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceStub.java @@ -5,7 +5,7 @@ public class AccessControlServiceStub extends AccessControlService { public AccessControlServiceStub() { - super(null, null, null, null, null); + super(null, null, null, null, null, null, null); } @Override diff --git a/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceTest.java index 446384e08..7a16fa2f8 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceTest.java @@ -27,7 +27,7 @@ public void setup() { @Test public void adminHasPrivilegeForNonTxn() { - AccessControlService accessControlService = new AccessControlService(userRepository, null, null, null, privilegeRepository); + AccessControlService accessControlService = new AccessControlService(userRepository, null, null, null, privilegeRepository, null, null); User user = new UserBuilder().id(1L).isAdmin(true).build(); when(privilegeRepository.isAllowedForAdmin(PrivilegeType.EditSubjectType)).thenReturn(true); accessControlService.checkPrivilege(user, PrivilegeType.EditSubjectType); @@ -36,7 +36,7 @@ public void adminHasPrivilegeForNonTxn() { @Test(expected = AvniAccessException.class) public void adminDoesntHavePrivilegeTxn() { initMocks(this); - AccessControlService accessControlService = new AccessControlService(userRepository, null, null, null, privilegeRepository); + AccessControlService accessControlService = new AccessControlService(userRepository, null, null, null, privilegeRepository, null, null); User user = new UserBuilder().id(1L).isAdmin(true).build(); when(privilegeRepository.isAllowedForAdmin(PrivilegeType.EditSubject)).thenReturn(false); accessControlService.checkPrivilege(user, PrivilegeType.EditSubject); From 49ace243e344c6049c9d682cd244a0b3c7d99fbc Mon Sep 17 00:00:00 2001 From: Joy A Date: Mon, 5 Aug 2024 11:01:16 +0530 Subject: [PATCH 057/129] #747 | /api/subjectMigration/bulk tests and code review comments --- .../service/SubjectMigrationService.java | 38 +++++-- .../web/SubjectMigrationController.java | 19 ++-- .../src/main/resources/api/external-api.yaml | 8 +- .../dao/SubjectMigrationIntegrationTest.java | 106 +++++++++++++++++- .../domain/factory/AddressLevelBuilder.java | 5 + 5 files changed, 149 insertions(+), 27 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java index c3be7f16b..7f01a2446 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/SubjectMigrationService.java @@ -12,6 +12,8 @@ import org.joda.time.DateTime; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import javax.transaction.Transactional; @@ -36,6 +38,7 @@ public class SubjectMigrationService implements ScopeAwareService bulkMigrate(BulkSubjectMigrationModes mode, BulkSubjectMigrationRequest bulkSubjectMigrationRequest) { if (mode == BulkSubjectMigrationModes.byAddress) { return bulkMigrateByAddress(bulkSubjectMigrationRequest.getSubjectIds(), bulkSubjectMigrationRequest.getDestinationAddresses()); @@ -163,6 +168,7 @@ public Map bulkMigrate(BulkSubjectMigrationModes mode, BulkSubje } } + @Transactional public Map bulkMigrateByAddress(List subjectIds, Map destinationAddresses) { Map migrationFailures = new HashMap<>(); Map addressLevelMap = new HashMap<>(); @@ -197,6 +203,7 @@ public Map bulkMigrateByAddress(List subjectIds, Map bulkMigrateBySyncConcept(List subjectIds, Map destinationSyncConcepts) { Map migrationFailures = new HashMap<>(); subjectIds.forEach(subjectId -> { @@ -219,16 +226,18 @@ public Map bulkMigrateBySyncConcept(List subjectIds, Map destinationSyncConcepts) { - String destinationSyncConceptValue = destinationSyncConcepts.get(subjectTypeSyncConceptUuid); - if (subjectTypeSyncConceptUuid != null && destinationSyncConceptValue == null) { - return null; // No migration required for this sync concept. + if (subjectTypeSyncConceptUuid == null || //sync concept not configured for subject type + !destinationSyncConcepts.containsKey(subjectTypeSyncConceptUuid) //sync concept not included in migration + ) { + return null; } - if (subjectTypeSyncConceptUuid == null) { - throw new RuntimeException("No sync concept configured for subject type"); + String destinationSyncConceptValue = destinationSyncConcepts.get(subjectTypeSyncConceptUuid); + if (destinationSyncConceptValue == null) { + return null; } Concept syncConcept = conceptRepository.findByUuid(subjectTypeSyncConceptUuid); - if (Objects.equals(currentValue, destinationSyncConceptValue)) { + if (currentValue != null && Objects.equals(currentValue.trim(), destinationSyncConceptValue.trim())) { throw new RuntimeException("Source value and Destination value are the same"); } @@ -243,12 +252,19 @@ private String validateSyncConcept(String subjectTypeSyncConceptUuid, String cur private static ObservationCollection buildSyncConceptValueObservations(Individual subject, String destinationSyncConcept1Value, String destinationSyncConcept2Value) { ObservationCollection newObservations = new ObservationCollection(); - if (destinationSyncConcept1Value != null) { - newObservations.put(subject.getSubjectType().getSyncRegistrationConcept1(), destinationSyncConcept1Value.trim()); + //set observation for unchanged values if sync concept exists so unchanged sync concept values are not overwritten + if (subject.getSubjectType().getSyncRegistrationConcept1() != null) { + newObservations.put(subject.getSubjectType().getSyncRegistrationConcept1(), destinationSyncConcept1Value != null ? destinationSyncConcept1Value.trim() : subject.getSyncConcept1Value()); } - if (destinationSyncConcept2Value != null) { - newObservations.put(subject.getSubjectType().getSyncRegistrationConcept2(), destinationSyncConcept2Value.trim()); + if (subject.getSubjectType().getSyncRegistrationConcept2() != null) { + newObservations.put(subject.getSubjectType().getSyncRegistrationConcept2(), destinationSyncConcept2Value != null ? destinationSyncConcept2Value.trim() : subject.getSyncConcept2Value()); } return newObservations; } + + public JobStatus getBulkSubjectMigrationJobStatus(String jobUuid) { + String jobFilterCondition = " and uuid = '" + jobUuid + "'"; + Page jobStatuses = avniJobRepository.getJobStatuses(UserContextHolder.getUser(), jobFilterCondition, PageRequest.of(0, 1)); + return (jobStatuses != null && !jobStatuses.getContent().isEmpty()) ? jobStatuses.getContent().get(0) : null; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java index 73ac7928a..7a0395040 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java @@ -54,10 +54,9 @@ public class SubjectMigrationController extends AbstractController scopeBasedSyncService, SubjectMigrationService subjectMigrationService, IndividualRepository individualRepository, LocationRepository locationRepository, AccessControlService accessControlService, Job bulkSubjectMigrationJob, JobLauncher bulkSubjectMigrationJobLauncher, AvniJobRepository avniJobRepository) { + public SubjectMigrationController(SubjectMigrationRepository subjectMigrationRepository, SubjectTypeRepository subjectTypeRepository, UserService userService, ScopeBasedSyncService scopeBasedSyncService, SubjectMigrationService subjectMigrationService, IndividualRepository individualRepository, LocationRepository locationRepository, AccessControlService accessControlService, Job bulkSubjectMigrationJob, JobLauncher bulkSubjectMigrationJobLauncher) { this.scopeBasedSyncService = scopeBasedSyncService; this.subjectMigrationService = subjectMigrationService; this.individualRepository = individualRepository; @@ -69,7 +68,6 @@ public SubjectMigrationController(SubjectMigrationRepository subjectMigrationRep this.subjectMigrationRepository = subjectMigrationRepository; this.subjectTypeRepository = subjectTypeRepository; this.userService = userService; - this.avniJobRepository = avniJobRepository; } @RequestMapping(value = "/subjectMigrations/v2", method = RequestMethod.GET) @@ -122,10 +120,12 @@ public ResponseEntity migrate(@RequestParam(value = "mode", defaultValue = "byAd if (bulkSubjectMigrationRequest.getSubjectIds() == null) { throw new BadRequestError("subjectIds is required"); } - if (mode == SubjectMigrationService.BulkSubjectMigrationModes.byAddress && bulkSubjectMigrationRequest.getDestinationAddresses() == null) { + if (mode == SubjectMigrationService.BulkSubjectMigrationModes.byAddress + && (bulkSubjectMigrationRequest.getDestinationAddresses() == null || bulkSubjectMigrationRequest.getDestinationAddresses().isEmpty())) { throw new BadRequestError("destinationAddresses is required for mode: byAddress"); } - if (mode == SubjectMigrationService.BulkSubjectMigrationModes.bySyncConcept && bulkSubjectMigrationRequest.getDestinationSyncConcepts() == null) { + if (mode == SubjectMigrationService.BulkSubjectMigrationModes.bySyncConcept + && (bulkSubjectMigrationRequest.getDestinationSyncConcepts() == null || bulkSubjectMigrationRequest.getDestinationSyncConcepts().isEmpty())) { throw new BadRequestError("destinationSyncConcepts is required for mode: bySyncConcepts"); } @@ -151,15 +151,14 @@ public ResponseEntity migrate(@RequestParam(value = "mode", defaultValue = "byAd throw new RuntimeException(String.format("Error while starting the bulk subject migration job, %s", e.getMessage()), e); } - return ResponseEntity.status(HttpStatus.ACCEPTED).body(migrationStatus(jobUUID)); + return ResponseEntity.status(HttpStatus.ACCEPTED).body(subjectMigrationService.getBulkSubjectMigrationJobStatus(jobUUID)); } @RequestMapping(value = "/api/subjectMigration/bulk/status/{jobUuid}", method = RequestMethod.GET) @PreAuthorize(value = "hasAnyAuthority('user')") - public JobStatus migrationStatus(@PathVariable("jobUuid") String jobUuid) { + public ResponseEntity migrationStatus(@PathVariable("jobUuid") String jobUuid) { accessControlService.checkPrivilege(PrivilegeType.MultiTxEntityTypeUpdate); - String jobFilterCondition = " and uuid = '" + jobUuid + "'"; - Page jobStatuses = avniJobRepository.getJobStatuses(UserContextHolder.getUser(), jobFilterCondition, PageRequest.of(0, 1)); - return (jobStatuses != null && !jobStatuses.getContent().isEmpty()) ? jobStatuses.getContent().get(0) : null; + JobStatus jobStatus = subjectMigrationService.getBulkSubjectMigrationJobStatus(jobUuid); + return jobStatus != null ? ResponseEntity.ok(jobStatus) : ResponseEntity.notFound().build(); } } diff --git a/avni-server-api/src/main/resources/api/external-api.yaml b/avni-server-api/src/main/resources/api/external-api.yaml index 94f5f960f..b16b56e14 100644 --- a/avni-server-api/src/main/resources/api/external-api.yaml +++ b/avni-server-api/src/main/resources/api/external-api.yaml @@ -1247,8 +1247,8 @@ paths: "5ecbc89b-3d06-4911-95c3-8227e9f78790": "Karnataka" }, "subjectIds": [ - "c47cc708-5707-42ee-8f27-098a567c3229", - "329760fe-aa7e-448a-9b48-2a3aaba7ffa5" + 1234, + 1235 ] } required: true @@ -1300,8 +1300,8 @@ paths: application/json: schema: $ref: '#/components/schemas/JobStatusBody' - "400": - description: bad input parameter + "404": + description: Job not found /api/subjectTree: delete: tags: diff --git a/avni-server-api/src/test/java/org/avni/server/dao/SubjectMigrationIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/dao/SubjectMigrationIntegrationTest.java index 79969bfa4..cd4f319c4 100644 --- a/avni-server-api/src/test/java/org/avni/server/dao/SubjectMigrationIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/dao/SubjectMigrationIntegrationTest.java @@ -3,6 +3,7 @@ import org.avni.server.common.AbstractControllerIntegrationTest; import org.avni.server.dao.sync.SyncEntityName; import org.avni.server.domain.*; +import org.avni.server.domain.factory.AddressLevelBuilder; import org.avni.server.domain.factory.TestUserSyncSettingsBuilder; import org.avni.server.domain.factory.UserBuilder; import org.avni.server.domain.factory.txData.ObservationCollectionBuilder; @@ -24,8 +25,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.test.context.jdbc.Sql; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import static org.junit.Assert.*; @@ -49,10 +49,13 @@ public class SubjectMigrationIntegrationTest extends AbstractControllerIntegrati private SyncController syncController; @Autowired private TestGroupService testGroupService; + @Autowired + private TestLocationService testLocationService; private Concept concept1; private Concept concept2; private SubjectType subjectType; + private SubjectType subjectTypeWithBothSyncRegistrationConcepts; private TestDataSetupService.TestCatchmentData catchmentData; private TestDataSetupService.TestOrganisationData organisationData; @@ -87,6 +90,17 @@ public void setup() { .setSyncRegistrationConcept1Usable(true) .setSyncRegistrationConcept1(concept1.getUuid()) .build()); + subjectTypeWithBothSyncRegistrationConcepts = testSubjectTypeService.createWithDefaults( + new SubjectTypeBuilder() + .setMandatoryFieldsForNewEntity() + .setUuid("subjectTypeWithBothSyncRegistrationConcepts") + .setName("subjectTypeWithBothSyncRegistrationConcepts") + .setSyncRegistrationConcept1(concept1.getUuid()) + .setSyncRegistrationConcept1Usable(true) + .setSyncRegistrationConcept2(concept2.getUuid()) + .setSyncRegistrationConcept2Usable(true) + .build() + ); testGroupService.giveViewSubjectPrivilegeTo(organisationData.getGroup(), subjectType); UserSyncSettings userSyncSettings = new TestUserSyncSettingsBuilder().setSubjectTypeUUID(subjectType.getUuid()).setSyncConcept1(concept1.getUuid()).setSyncConcept1Values(Collections.singletonList(concept1.getAnswerConcept("Answer 11").getUuid())).build(); userRepository.save(new UserBuilder(organisationData.getUser()).withCatchment(catchmentData.getCatchment()).withOperatingIndividualScope(OperatingIndividualScope.ByCatchment).withSubjectTypeSyncSettings(userSyncSettings).build()); @@ -189,4 +203,92 @@ public void migrations_created_by_one_user_is_returned_for_another_user_even_whe assertTrue(getSyncDetails().contains(EntitySyncStatusContract.createForComparison(SyncEntityName.SubjectMigration.name(), subjectType.getUuid()))); assertEquals(1, getMigrations(subjectType, DateTime.now().minusDays(1), DateTime.now()).size()); } + + @Test + public void bulkMigrateTracksFailures() { + Individual i = testSubjectService.save(new SubjectBuilder().withMandatoryFieldsForNewEntity().withSubjectType(subjectType).withLocation(catchmentData.getAddressLevel1()).withObservations(ObservationCollectionBuilder.withOneObservation(concept1, concept1.getAnswerConcept("Answer 11").getUuid())).build()); + AddressLevel voidedAddressLevel = new AddressLevelBuilder().withDefaultValuesForNewEntity().type(catchmentData.getAddressLevelType()).voided(true).build(); + testLocationService.save(voidedAddressLevel); + testGroupService.giveEditSubjectPrivilegeTo(organisationData.getGroup(), subjectType); + Map destinationAddressLevels = new HashMap<>(); + destinationAddressLevels.put(String.valueOf(i.getAddressLevel().getId()), String.valueOf(voidedAddressLevel.getId())); + Map failures = subjectMigrationService.bulkMigrateByAddress(Collections.singletonList(i.getId()), destinationAddressLevels); + assertEquals(failures.size(), 1); + } + + @Test + public void bulkMigrateByAddressLevelSucceedsForValidDestinationAddress() { + Individual i = testSubjectService.save(new SubjectBuilder().withMandatoryFieldsForNewEntity().withSubjectType(subjectType).withLocation(catchmentData.getAddressLevel1()).withObservations(ObservationCollectionBuilder.withOneObservation(concept1, concept1.getAnswerConcept("Answer 11").getUuid())).build()); + Map destinationAddressLevels = new HashMap<>(); + destinationAddressLevels.put(i.getAddressLevel().getId().toString(), catchmentData.getAddressLevel2().getId().toString()); + testGroupService.giveEditSubjectPrivilegeTo(organisationData.getGroup(), subjectType); + Map failures = subjectMigrationService.bulkMigrateByAddress(Collections.singletonList(i.getId()), destinationAddressLevels); + assertTrue(failures.isEmpty()); + i = testSubjectService.reload(i); + assertEquals(i.getAddressLevel(), catchmentData.getAddressLevel2()); + assertTrue(hasMigrationFor(subjectType, DateTime.now().minusDays(1), DateTime.now(), i)); + } + + @Test + public void bulkMigrateProcessesAllRecordsEvenIfOneFails() { + Individual i1 = testSubjectService.save(new SubjectBuilder().withMandatoryFieldsForNewEntity().withSubjectType(subjectType).withLocation(catchmentData.getAddressLevel1()).withObservations(ObservationCollectionBuilder.withOneObservation(concept1, concept1.getAnswerConcept("Answer 11").getUuid())).build()); + Individual i2 = testSubjectService.save(new SubjectBuilder().withMandatoryFieldsForNewEntity().withSubjectType(subjectType).withLocation(catchmentData.getAddressLevel2()).withObservations(ObservationCollectionBuilder.withOneObservation(concept1, concept1.getAnswerConcept("Answer 12").getUuid())).build()); + AddressLevel voidedAddressLevel = new AddressLevelBuilder().withDefaultValuesForNewEntity().type(catchmentData.getAddressLevelType()).voided(true).build(); + testLocationService.save(voidedAddressLevel); + Map destinationAddressLevels = new HashMap<>(); + destinationAddressLevels.put(catchmentData.getAddressLevel1().getId().toString(), voidedAddressLevel.getId().toString()); + destinationAddressLevels.put(catchmentData.getAddressLevel2().getId().toString(), catchmentData.getAddressLevel1().getId().toString()); + testGroupService.giveEditSubjectPrivilegeTo(organisationData.getGroup(), subjectType); + Map failures = subjectMigrationService.bulkMigrateByAddress(Arrays.asList(i1.getId(), i2.getId()), destinationAddressLevels); + assertEquals(failures.size(), 1); + i1 = testSubjectService.reload(i1); + i2 = testSubjectService.reload(i2); + assertEquals(i1.getAddressLevel(), catchmentData.getAddressLevel1()); + assertFalse(hasMigrationFor(subjectType, DateTime.now().minusDays(1), DateTime.now(), i1)); + assertEquals(i2.getAddressLevel(), catchmentData.getAddressLevel1()); + assertTrue(hasMigrationFor(subjectType, DateTime.now().minusDays(1), DateTime.now(), i2)); + } + + @Test + public void bulkMigrateBySyncConceptMigratesSyncConcept1ValueIfSyncConcept2IsNotConfiguredForSubjectType() { + Individual i = testSubjectService.save(new SubjectBuilder().withMandatoryFieldsForNewEntity().withSubjectType(subjectType).withLocation(catchmentData.getAddressLevel1()).withObservations(ObservationCollectionBuilder.withOneObservation(concept1, concept1.getAnswerConcept("Answer 11").getUuid())).build()); + Map destinationSyncConcepts = new HashMap<>(); + destinationSyncConcepts.put(subjectType.getSyncRegistrationConcept1(), concept1.getAnswerConcept("Answer 12").getUuid()); + testGroupService.giveEditSubjectPrivilegeTo(organisationData.getGroup(), subjectType); + Map failures = subjectMigrationService.bulkMigrateBySyncConcept(Collections.singletonList(i.getId()), destinationSyncConcepts); + assertEquals(failures.size(), 0); + i = testSubjectService.reload(i); + assertEquals(i.getSyncConcept1Value(), concept1.getAnswerConcept("Answer 12").getUuid()); + assertNull(i.getSyncConcept2Value()); + } + + @Test + public void bulkMigrateBySyncConceptMigratesSyncConcept2ValueEvenIfSyncConcept1ValueDoesNotNeedToBeMigrated() { + Individual i1 = testSubjectService.save(new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectTypeWithBothSyncRegistrationConcepts) + .withLocation(catchmentData.getAddressLevel1()) + .withObservations(new ObservationCollectionBuilder() + .addObservation(concept1, concept1.getAnswerConcept("Answer 11").getUuid()) + .addObservation(concept2, concept2.getAnswerConcept("Answer 21").getUuid()) + .build()) + .build()); + Map destinationSyncConcepts = new HashMap<>(); + destinationSyncConcepts.put(subjectTypeWithBothSyncRegistrationConcepts.getSyncRegistrationConcept2(), concept2.getAnswerConcept("Answer 22").getUuid()); + testGroupService.giveViewSubjectPrivilegeTo(organisationData.getGroup(), subjectTypeWithBothSyncRegistrationConcepts); + testGroupService.giveEditSubjectPrivilegeTo(organisationData.getGroup(), subjectTypeWithBothSyncRegistrationConcepts); + Map failures = subjectMigrationService.bulkMigrateBySyncConcept(Collections.singletonList(i1.getId()), destinationSyncConcepts); + assertEquals(failures.size(), 0); + i1 = testSubjectService.reload(i1); + assertEquals(i1.getSyncConcept1Value(), concept1.getAnswerConcept("Answer 11").getUuid()); + assertEquals(i1.getSyncConcept2Value(), concept2.getAnswerConcept("Answer 22").getUuid()); + UserSyncSettings userSyncSettings = new TestUserSyncSettingsBuilder().setSubjectTypeUUID(subjectTypeWithBothSyncRegistrationConcepts.getUuid()) + .setSyncConcept1(concept1.getUuid()) + .setSyncConcept1Values(Collections.singletonList(concept1.getAnswerConcept("Answer 11").getUuid())) + .setSyncConcept2(concept2.getUuid()) + .setSyncConcept2Values(Collections.singletonList(concept2.getAnswerConcept("Answer 22").getUuid())) + .build(); + userRepository.save(new UserBuilder(organisationData.getUser()).withCatchment(catchmentData.getCatchment()).withOperatingIndividualScope(OperatingIndividualScope.ByCatchment).withSubjectTypeSyncSettings(userSyncSettings).build()); + setUser(organisationData.getUser().getUsername()); + assertTrue(hasMigrationFor(subjectTypeWithBothSyncRegistrationConcepts, DateTime.now().minusDays(1), DateTime.now(), i1)); + } } diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java index 511c61e9e..59d78913d 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java @@ -37,6 +37,11 @@ public AddressLevelBuilder withUuid(String uuid) { return this; } + public AddressLevelBuilder voided(boolean voided) { + entity.setVoided(voided); + return this; + } + public AddressLevelBuilder withDefaultValuesForNewEntity() { String s = UUID.randomUUID().toString(); return withUuid(s).title(s); From 7aa087a7e572f5ddc8622785b6954da5757fa3f4 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 5 Aug 2024 13:44:35 +0530 Subject: [PATCH 058/129] avniproject/avni-webapp#1298 - applied partition check to all web controllers methods --- .../SubjectPartitionCheckStatus.java | 8 +++++ .../accessControl/SubjectPartitionData.java | 7 ++-- .../server/service/IndividualService.java | 36 ++++++++++--------- .../accessControl/AccessControlService.java | 16 +++++---- .../avni/server/web/EncounterController.java | 21 ++++++----- .../avni/server/web/IndividualController.java | 34 ++++++++++-------- .../web/ProgramEncounterController.java | 10 ++++-- .../web/ProgramEnrolmentController.java | 12 ++++--- .../server/web/TxDataControllerHelper.java | 28 +++++++++++++++ .../AccessControlServiceStub.java | 2 +- .../AccessControlServiceTest.java | 4 +-- 11 files changed, 118 insertions(+), 60 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/web/TxDataControllerHelper.java diff --git a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java index 536dee909..b4f45e70a 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java @@ -22,4 +22,12 @@ public static SubjectPartitionCheckStatus passed() { public static SubjectPartitionCheckStatus failed(String message) { return new SubjectPartitionCheckStatus(false, message); } + + public boolean isPassed() { + return passed; + } + + public String getMessage() { + return message; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionData.java b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionData.java index 7ffeb481d..bbc4ab325 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionData.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionData.java @@ -9,7 +9,7 @@ public class SubjectPartitionData { private final String sync1ConceptValue; private final String sync2ConceptValue; - public SubjectPartitionData(AddressLevel addressLevel, String sync1ConceptValue, String sync2ConceptValue) { + private SubjectPartitionData(AddressLevel addressLevel, String sync1ConceptValue, String sync2ConceptValue) { if (addressLevel == null && StringUtils.isEmpty(sync1ConceptValue)) { throw new IllegalArgumentException("AddressLevel or sync1ConceptValue must be provided"); } @@ -18,8 +18,9 @@ public SubjectPartitionData(AddressLevel addressLevel, String sync1ConceptValue, this.sync2ConceptValue = sync2ConceptValue; } - public SubjectPartitionData(Individual subject) { - this(subject.getAddressLevel(), subject.getSyncConcept1Value(), subject.getSyncConcept2Value()); + public static SubjectPartitionData create(Individual subject) { + if (subject == null) return null; + return new SubjectPartitionData(subject.getAddressLevel(), subject.getSyncConcept1Value(), subject.getSyncConcept2Value()); } public AddressLevel getAddressLevel() { diff --git a/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java b/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java index d57a9c778..0fa94ae59 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IndividualService.java @@ -14,6 +14,8 @@ import org.avni.server.dao.sync.SyncEntityName; import org.avni.server.domain.*; import org.avni.server.domain.accessControl.PrivilegeType; +import org.avni.server.domain.accessControl.SubjectPartitionCheckStatus; +import org.avni.server.domain.accessControl.SubjectPartitionData; import org.avni.server.domain.individualRelationship.IndividualRelation; import org.avni.server.domain.individualRelationship.IndividualRelationship; import org.avni.server.domain.observation.PhoneNumber; @@ -90,13 +92,13 @@ public Individual findById(Long id) { public Individual findByMetadata(String subjectTypeName, String programName, String encounterTypeName, long entityId) { Individual individual = null; - if(subjectTypeName != null && programName == null && encounterTypeName == null) { + if (subjectTypeName != null && programName == null && encounterTypeName == null) { individual = individualRepository.findById(entityId).get(); - } else if(programName == null && encounterTypeName != null) { + } else if (programName == null && encounterTypeName != null) { individual = encounterRepository.findById(entityId).get().getIndividual(); - } else if(programName != null && encounterTypeName == null) { + } else if (programName != null && encounterTypeName == null) { individual = programEnrolmentRepository.findById(entityId).get().getIndividual(); - } else if(programName != null && encounterTypeName != null) { + } else if (programName != null && encounterTypeName != null) { individual = programEncounterRepository.findById(entityId).get().getIndividual(); } return individual; @@ -152,7 +154,7 @@ public IndividualContract getSubjectInfo(String individualUuid) { individualContract.setMiddleName(individual.getMiddleName()); individualContract.setLastName(individual.getLastName()); if (null != individual.getProfilePicture() - && individual.getSubjectType().isAllowProfilePicture()) + && individual.getSubjectType().isAllowProfilePicture()) individualContract.setProfilePicture(individual.getProfilePicture()); if (null != individual.getDateOfBirth()) individualContract.setDateOfBirth(individual.getDateOfBirth()); @@ -226,7 +228,7 @@ public Set constructProgramEncounters(Stream { ProgramEncounterContract programEncountersContract = new ProgramEncounterContract(); EntityTypeContract entityTypeContract = - EntityTypeContract.fromEncounterType(programEncounter.getEncounterType()); + EntityTypeContract.fromEncounterType(programEncounter.getEncounterType()); programEncountersContract.setUuid(programEncounter.getUuid()); programEncountersContract.setId(programEncounter.getId()); programEncountersContract.setName(programEncounter.getName()); @@ -353,9 +355,9 @@ public Individual voidSubject(Individual individual) { private void assertNoUnVoidedEnrolments(Individual individual) { long nonVoidedProgramEnrolments = individual.getProgramEnrolments() - .stream() - .filter(pe -> !pe.isVoided()) - .count(); + .stream() + .filter(pe -> !pe.isVoided()) + .count(); if (nonVoidedProgramEnrolments != 0) { throw new BadRequestError(String.format("There are non deleted program enrolments for the %s %s", individual.getSubjectType().getOperationalSubjectTypeName(), individual.getFirstName())); } @@ -386,8 +388,8 @@ public Object getObservationValueForUpload(FormElement formElement, String answe if (formElement.getType().equals(FormElementType.MultiSelect.name())) { String[] providedAnswers = S.splitMultiSelectAnswer(answerValue); return Stream.of(providedAnswers) - .map(answer -> individualRepository.findByLegacyIdOrUuidAndSubjectType(answer, subjectType).getUuid()) - .collect(Collectors.toList()); + .map(answer -> individualRepository.findByLegacyIdOrUuidAndSubjectType(answer, subjectType).getUuid()) + .collect(Collectors.toList()); } else { return individualRepository.findByLegacyIdOrUuidAndSubjectType(answerValue, subjectType).getUuid(); } @@ -414,8 +416,8 @@ public String findPhoneNumber(Individual individual) { Optional phoneNumberConcept = conceptRepository.findAllByDataType("PhoneNumber").stream().findFirst(); if (phoneNumberConcept.isPresent()) { Optional phoneNumber = individual.getObservations().entrySet().stream().filter(entrySet -> - Objects.equals(entrySet.getKey(), phoneNumberConcept.get().getUuid())) - .map(phoneNumberEntry -> objectMapper.convertValue(phoneNumberEntry.getValue(), PhoneNumber.class).getPhoneNumber()).findFirst(); + Objects.equals(entrySet.getKey(), phoneNumberConcept.get().getUuid())) + .map(phoneNumberEntry -> objectMapper.convertValue(phoneNumberEntry.getValue(), PhoneNumber.class).getPhoneNumber()).findFirst(); if (phoneNumber.isPresent()) { return phoneNumber.get(); } @@ -423,8 +425,8 @@ public String findPhoneNumber(Individual individual) { Optional phoneNumberTextConcept = conceptService.findContactNumberConcept(); if (phoneNumberTextConcept.isPresent()) { Optional phoneNumber = individual.getObservations().entrySet().stream().filter(entrySet -> - Objects.equals(entrySet.getKey(), phoneNumberTextConcept.get().getUuid())) - .map(stringObjectEntry -> (String) (stringObjectEntry.getValue())).findFirst(); + Objects.equals(entrySet.getKey(), phoneNumberTextConcept.get().getUuid())) + .map(stringObjectEntry -> (String) (stringObjectEntry.getValue())).findFirst(); if (phoneNumber.isPresent()) { return phoneNumber.get(); } @@ -463,8 +465,8 @@ public Optional findByPhoneNumber(String phoneNumber) { } phoneNumber = phoneNumber.substring(phoneNumber.length() - NO_OF_DIGITS_IN_INDIAN_MOBILE_NO); return phoneNumberConcept.isPresent() - ? individualRepository.findByConceptWithMatchingPattern(phoneNumberConcept.get(), "%" + phoneNumber) - : Optional.empty(); + ? individualRepository.findByConceptWithMatchingPattern(phoneNumberConcept.get(), "%" + phoneNumber) + : Optional.empty(); } @Transactional diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java index 286c64b70..f4f66d8fc 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java @@ -5,6 +5,7 @@ import org.avni.server.domain.accessControl.*; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.CatchmentService; +import org.avni.server.service.UserService; import org.avni.server.service.UserSubjectAssignmentService; import org.avni.server.web.request.syncAttribute.UserSyncSettings; import org.springframework.beans.factory.annotation.Autowired; @@ -26,9 +27,10 @@ public class AccessControlService { private final PrivilegeRepository privilegeRepository; private final CatchmentService catchmentService; private final UserSubjectAssignmentService userSubjectAssignmentService; + private final UserService userService; @Autowired - public AccessControlService(UserRepository userRepository, SubjectTypeRepository subjectTypeRepository, ProgramRepository programRepository, EncounterTypeRepository encounterTypeRepository, PrivilegeRepository privilegeRepository, CatchmentService catchmentService, UserSubjectAssignmentService userSubjectAssignmentService) { + public AccessControlService(UserRepository userRepository, SubjectTypeRepository subjectTypeRepository, ProgramRepository programRepository, EncounterTypeRepository encounterTypeRepository, PrivilegeRepository privilegeRepository, CatchmentService catchmentService, UserSubjectAssignmentService userSubjectAssignmentService, UserService userService) { this.userRepository = userRepository; this.subjectTypeRepository = subjectTypeRepository; this.programRepository = programRepository; @@ -36,6 +38,7 @@ public AccessControlService(UserRepository userRepository, SubjectTypeRepository this.privilegeRepository = privilegeRepository; this.catchmentService = catchmentService; this.userSubjectAssignmentService = userSubjectAssignmentService; + this.userService = userService; } public void checkPrivilege(PrivilegeType privilegeType) { @@ -218,16 +221,17 @@ public void checkApprovePrivilegeOnEntityApprovalStatuses(List syncSettingsList = user.getSyncSettingsList(); + List syncSettingsList = currentUser.getSyncSettingsList(); UserSyncSettings userSyncSettingsForSubjectType = syncSettingsList.stream().filter(userSyncSettings -> userSyncSettings.getSubjectTypeUUID().equals(subjectType.getUuid())).findFirst().orElse(null); if (userSyncSettingsForSubjectType == null) { return SubjectPartitionCheckStatus.failed(SubjectPartitionCheckStatus.UserSyncAttributeNotConfigured); diff --git a/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java b/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java index f39e2d778..c9d493cfc 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java @@ -54,6 +54,7 @@ public class EncounterController extends AbstractController implement private final FormMappingService formMappingService; private final AccessControlService accessControlService; private final EntityApprovalStatusService entityApprovalStatusService; + private final TxDataControllerHelper txDataControllerHelper; @Autowired public EncounterController(IndividualRepository individualRepository, @@ -62,7 +63,7 @@ public EncounterController(IndividualRepository individualRepository, ObservationService observationService, UserService userService, Bugsnag bugsnag, - EncounterService encounterService, ScopeBasedSyncService scopeBasedSyncService, FormMappingService formMappingService, AccessControlService accessControlService, EntityApprovalStatusService entityApprovalStatusService) { + EncounterService encounterService, ScopeBasedSyncService scopeBasedSyncService, FormMappingService formMappingService, AccessControlService accessControlService, EntityApprovalStatusService entityApprovalStatusService, TxDataControllerHelper txDataControllerHelper) { this.individualRepository = individualRepository; this.encounterTypeRepository = encounterTypeRepository; this.encounterRepository = encounterRepository; @@ -74,6 +75,7 @@ public EncounterController(IndividualRepository individualRepository, this.formMappingService = formMappingService; this.accessControlService = accessControlService; this.entityApprovalStatusService = entityApprovalStatusService; + this.txDataControllerHelper = txDataControllerHelper; } @GetMapping(value = "/web/encounter/{uuid}") @@ -102,7 +104,7 @@ private void checkForSchedulingCompleteConstraintViolation(EncounterRequest requ public void save(@RequestBody EncounterRequest request) { logger.info(String.format("Saving encounter with uuid %s", request.getUuid())); - createEncounter(request, encounterService); + createEncounter(request); logger.info(String.format("Saved encounter with uuid %s", request.getUuid())); } @@ -111,9 +113,11 @@ public void save(@RequestBody EncounterRequest request) { @Transactional @PreAuthorize(value = "hasAnyAuthority('user')") public void saveForWeb(@RequestBody EncounterRequest request) { - logger.info("Saving encounter with uuid %s", request.getUuid()); + logger.info("Saving encounter with uuid {}}", request.getUuid()); - Encounter encounter = createEncounter(request, encounterService); + Encounter encounter = createEncounter(request); + if (encounter != null) // create encounter method needs fixing. it should not return null in any case + txDataControllerHelper.checkSubjectAccess(encounter.getIndividual()); addEntityApprovalStatusIfRequired(encounter); logger.info(String.format("Saved encounter with uuid %s", request.getUuid())); @@ -125,8 +129,7 @@ private void addEntityApprovalStatusIfRequired(Encounter encounter) { entityApprovalStatusService.createStatus(EntityApprovalStatus.EntityType.Encounter, encounter.getId(), ApprovalStatus.Status.Pending, encounter.getEncounterType().getUuid(), formMapping); } - private Encounter createEncounter(EncounterRequest request, EncounterService encounterService) { - + private Encounter createEncounter(EncounterRequest request) { checkForSchedulingCompleteConstraintViolation(request); EncounterType encounterType = encounterTypeRepository.findByUuidOrName(request.getEncounterTypeUUID(), request.getEncounterType()); @@ -138,6 +141,7 @@ private Encounter createEncounter(EncounterRequest request, EncounterService enc } Encounter encounter = newOrExistingEntity(encounterRepository, request, new Encounter()); + encounter.setIndividual(individual); //Planned visit can not overwrite completed encounter if (encounter.isCompleted() && request.isPlanned()) return null; @@ -152,9 +156,7 @@ private Encounter createEncounter(EncounterRequest request, EncounterService enc bugsnag.notify(new Exception(errorMessage)); logger.error(errorMessage); } - encounter.setEncounterDateTime(request.getEncounterDateTime(), userService.getCurrentUser()); - encounter.setIndividual(individual); encounter.setEncounterType(encounterType); encounter.setObservations(observationService.createObservations(request.getObservations())); encounter.setName(request.getName()); @@ -187,7 +189,7 @@ private Encounter createEncounter(EncounterRequest request, EncounterService enc } this.encounterService.save(encounter); - if (request.getVisitSchedules() != null && request.getVisitSchedules().size() > 0) { + if (request.getVisitSchedules() != null && !request.getVisitSchedules().isEmpty()) { this.encounterService.saveVisitSchedules(individual.getUuid(), request.getVisitSchedules(), request.getUuid()); } return encounter; @@ -253,6 +255,7 @@ public ResponseEntity voidEncounter(@PathVariable String uuid) { if (encounter == null) { return ResponseEntity.notFound().build(); } + txDataControllerHelper.checkSubjectAccess(encounter.getIndividual()); accessControlService.checkEncounterPrivilege(PrivilegeType.VoidVisit, encounter); encounter.setVoided(true); encounterService.save(encounter); diff --git a/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java b/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java index 68d229896..1987b2570 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java @@ -7,6 +7,7 @@ import org.avni.server.dao.sync.SyncEntityName; import org.avni.server.domain.*; import org.avni.server.domain.accessControl.PrivilegeType; +import org.avni.server.domain.accessControl.SubjectPartitionData; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.geo.Point; import org.avni.server.projection.IndividualWebProjection; @@ -72,6 +73,7 @@ public class IndividualController extends AbstractController impleme private final EntityApprovalStatusService entityApprovalStatusService; private final FormMappingService formMappingService; private final Bugsnag bugsnag; + private final TxDataControllerHelper txDataControllerHelper; @Autowired public IndividualController(IndividualRepository individualRepository, @@ -86,7 +88,7 @@ public IndividualController(IndividualRepository individualRepository, IndividualSearchService individualSearchService, IdentifierAssignmentRepository identifierAssignmentRepository, IndividualConstructionService individualConstructionService, - ScopeBasedSyncService scopeBasedSyncService, SubjectMigrationService subjectMigrationService, AccessControlService accessControlService, EntityApprovalStatusService entityApprovalStatusService, FormMappingService formMappingService, Bugsnag bugsnag) { + ScopeBasedSyncService scopeBasedSyncService, SubjectMigrationService subjectMigrationService, AccessControlService accessControlService, EntityApprovalStatusService entityApprovalStatusService, FormMappingService formMappingService, Bugsnag bugsnag, TxDataControllerHelper txDataControllerHelper) { this.individualRepository = individualRepository; this.locationRepository = locationRepository; this.genderRepository = genderRepository; @@ -105,6 +107,7 @@ public IndividualController(IndividualRepository individualRepository, this.entityApprovalStatusService = entityApprovalStatusService; this.formMappingService = formMappingService; this.bugsnag = bugsnag; + this.txDataControllerHelper = txDataControllerHelper; } // used in offline mode hence no access check @@ -182,13 +185,13 @@ public Page search( @RequestParam(value = "name", required = false) String name, @RequestParam(value = "subjectTypeUUID", required = false) String subjectTypeUUID, Pageable pageable) { - IndividualRepository repo = this.individualRepository; + IndividualRepository repo = this.individualRepository; return repo.findAll( - where(repo.getFilterSpecForName(name)) - .and(repo.getFilterSpecForSubjectTypeId(subjectTypeUUID)) - .and(repo.getFilterSpecForVoid(false)) - , pageable) - .map(t -> projectionFactory.createProjection(IndividualWebProjection.class, t)); + where(repo.getFilterSpecForName(name)) + .and(repo.getFilterSpecForSubjectTypeId(subjectTypeUUID)) + .and(repo.getFilterSpecForVoid(false)) + , pageable) + .map(t -> projectionFactory.createProjection(IndividualWebProjection.class, t)); } @PostMapping(value = "/web/searchAPI/v2") @@ -263,6 +266,7 @@ public Page getAllCompletedEncounters( @Transactional public ResponseEntity voidSubject(@PathVariable String uuid) { Individual individual = individualRepository.findByUuid(uuid); + txDataControllerHelper.checkSubjectAccess(individual); if (individual == null) { return ResponseEntity.notFound().build(); } @@ -308,14 +312,14 @@ public ResponseEntity findByMetadata( @Param(value = "programName") String programName, @Param(value = "encounterTypeName") String encounterTypeName, @RequestParam(value = "entityId") String entityId) { - try{ + try { Individual individual = this.individualService.findByMetadata(subjectTypeName, programName, encounterTypeName, Long.parseLong(entityId)); - if(individual == null) { + if (individual == null) { return ResponseEntity.badRequest().build(); } IndividualWebProjection individualWebProjection = this.projectionFactory.createProjection(IndividualWebProjection.class, individual); return ResponseEntity.ok(individualWebProjection); - }catch(NoSuchElementException e) { + } catch (NoSuchElementException e) { return ResponseEntity.notFound().build(); } } @@ -342,12 +346,15 @@ public Resource process(Resource resource) { @PreAuthorize(value = "hasAnyAuthority('user')") public AvniEntityResponse saveForWeb(@RequestBody IndividualRequest individualRequest) { logger.info(String.format("Saving individual with UUID %s", individualRequest.getUuid())); + Individual savedIndividual = individualService.getIndividual(individualRequest.getUuid()); + //Subject is changed after this line, hence the following line cannot be moved down closer to its usage + SubjectPartitionData subjectPartitionData = SubjectPartitionData.create(savedIndividual); Individual individual = createIndividual(individualRequest); FormMapping formMapping = formMappingService.findBy(individual.getSubjectType(), null, null, FormType.IndividualProfile); entityApprovalStatusService.createStatus(EntityApprovalStatus.EntityType.Subject, individual.getId(), ApprovalStatus.Status.Pending, individual.getSubjectType().getUuid(), formMapping); - + txDataControllerHelper.checkSubjectAccess(individual, subjectPartitionData); logger.info(String.format("Saved individual with UUID %s", individualRequest.getUuid())); return new AvniEntityResponse(individual); @@ -362,20 +369,19 @@ private Individual createIndividual(IndividualRequest individualRequest) { Individual individual = createIndividualWithoutObservations(individualRequest); - // Temporary fix to + // Temporary fix to not allow emptying of observations if ((individualRequest.getObservations() == null || individualRequest.getObservations().isEmpty()) && individual.getObservations() != null && !individual.getObservations().isEmpty()) { String errorMessage = String.format("Individual Observations not all allowed to be made empty. User: %s, UUID: %s, ", UserContextHolder.getUser().getUsername(), individualRequest.getUuid()); bugsnag.notify(new Exception(errorMessage)); logger.error(errorMessage); individual.updateAudit(); } else { - individual.setObservations(observations); + individual.setObservations(observations); } Individual savedIndividual = individualService.save(individual); saveVisitSchedules(individualRequest); saveIdentifierAssignments(savedIndividual, individualRequest); - return savedIndividual; } diff --git a/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java b/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java index 826db563e..22dc2ed03 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java @@ -43,9 +43,10 @@ public class ProgramEncounterController implements RestControllerResourceProcess private final FormMappingService formMappingService; private final AccessControlService accessControlService; private final EntityApprovalStatusService entityApprovalStatusService; + private final TxDataControllerHelper txDataControllerHelper; @Autowired - public ProgramEncounterController(EncounterTypeRepository encounterTypeRepository, ProgramEncounterRepository programEncounterRepository, UserService userService, ProgramEncounterService programEncounterService, ScopeBasedSyncService scopeBasedSyncService, FormMappingService formMappingService, AccessControlService accessControlService, EntityApprovalStatusService entityApprovalStatusService) { + public ProgramEncounterController(EncounterTypeRepository encounterTypeRepository, ProgramEncounterRepository programEncounterRepository, UserService userService, ProgramEncounterService programEncounterService, ScopeBasedSyncService scopeBasedSyncService, FormMappingService formMappingService, AccessControlService accessControlService, EntityApprovalStatusService entityApprovalStatusService, TxDataControllerHelper txDataControllerHelper) { this.encounterTypeRepository = encounterTypeRepository; this.programEncounterRepository = programEncounterRepository; this.userService = userService; @@ -54,6 +55,7 @@ public ProgramEncounterController(EncounterTypeRepository encounterTypeRepositor this.formMappingService = formMappingService; this.accessControlService = accessControlService; this.entityApprovalStatusService = entityApprovalStatusService; + this.txDataControllerHelper = txDataControllerHelper; } @GetMapping(value = "/web/programEncounter/{uuid}") @@ -72,7 +74,7 @@ public ResponseEntity getProgramEncounterByUuid(@PathV @PreAuthorize(value = "hasAnyAuthority('user')") public void save(@RequestBody ProgramEncounterRequest request) { programEncounterService.saveProgramEncounter(request); - if (request.getVisitSchedules() != null && request.getVisitSchedules().size() > 0) { + if (request.getVisitSchedules() != null && !request.getVisitSchedules().isEmpty()) { programEncounterService.saveVisitSchedules(request.getProgramEnrolmentUUID(), request.getVisitSchedules(), request.getUuid()); } } @@ -82,7 +84,8 @@ public void save(@RequestBody ProgramEncounterRequest request) { @PreAuthorize(value = "hasAnyAuthority('user')") public void saveForWeb(@RequestBody ProgramEncounterRequest request) { ProgramEncounter programEncounter = programEncounterService.saveProgramEncounter(request); - if (request.getVisitSchedules() != null && request.getVisitSchedules().size() > 0) { + txDataControllerHelper.checkSubjectAccess(programEncounter.getProgramEnrolment().getIndividual()); + if (request.getVisitSchedules() != null && !request.getVisitSchedules().isEmpty()) { programEncounterService.saveVisitSchedules(request.getProgramEnrolmentUUID(), request.getVisitSchedules(), request.getUuid()); } @@ -153,6 +156,7 @@ public ResponseEntity voidProgramEncounter(@PathVariable String uuid) { if (programEncounter == null) { return ResponseEntity.notFound().build(); } + txDataControllerHelper.checkSubjectAccess(programEncounter.getProgramEnrolment().getIndividual()); programEncounter.setVoided(true); programEncounterService.save(programEncounter); return ResponseEntity.ok().build(); diff --git a/avni-server-api/src/main/java/org/avni/server/web/ProgramEnrolmentController.java b/avni-server-api/src/main/java/org/avni/server/web/ProgramEnrolmentController.java index d79084241..f5a2f65a9 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ProgramEnrolmentController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ProgramEnrolmentController.java @@ -11,23 +11,22 @@ import org.avni.server.domain.ProgramEnrolment; import org.avni.server.projection.ProgramEnrolmentProjection; import org.avni.server.service.*; -import org.avni.server.service.accessControl.AccessControlService; import org.avni.server.web.request.EnrolmentContract; import org.avni.server.web.request.ProgramEncounterContract; import org.avni.server.web.request.ProgramEnrolmentRequest; import org.avni.server.web.response.AvniEntityResponse; import org.avni.server.web.response.slice.SlicedResources; -import org.springframework.hateoas.PagedResources; import org.joda.time.DateTime; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.SliceImpl; -import org.springframework.data.domain.PageImpl; import org.springframework.data.projection.ProjectionFactory; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.hateoas.Link; +import org.springframework.hateoas.PagedResources; import org.springframework.hateoas.Resource; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -37,7 +36,6 @@ import java.util.Collections; import static org.avni.server.web.resourceProcessors.ResourceProcessor.addAuditFields; -import static org.avni.server.web.resourceProcessors.ResourceProcessor.addUserFields; @RestController public class ProgramEnrolmentController extends AbstractController implements RestControllerResourceProcessor { @@ -50,9 +48,10 @@ public class ProgramEnrolmentController extends AbstractController scopeBasedSyncService; private final FormMappingService formMappingService; private final EntityApprovalStatusService entityApprovalStatusService; + private final TxDataControllerHelper txDataControllerHelper; @Autowired - public ProgramEnrolmentController(ProgramRepository programRepository, ProgramEnrolmentRepository programEnrolmentRepository, UserService userService, ProjectionFactory projectionFactory, ProgramEnrolmentService programEnrolmentService, ScopeBasedSyncService scopeBasedSyncService, FormMappingService formMappingService, EntityApprovalStatusService entityApprovalStatusService) { + public ProgramEnrolmentController(ProgramRepository programRepository, ProgramEnrolmentRepository programEnrolmentRepository, UserService userService, ProjectionFactory projectionFactory, ProgramEnrolmentService programEnrolmentService, ScopeBasedSyncService scopeBasedSyncService, FormMappingService formMappingService, EntityApprovalStatusService entityApprovalStatusService, TxDataControllerHelper txDataControllerHelper) { this.programEnrolmentRepository = programEnrolmentRepository; this.userService = userService; this.projectionFactory = projectionFactory; @@ -61,6 +60,7 @@ public ProgramEnrolmentController(ProgramRepository programRepository, ProgramEn this.scopeBasedSyncService = scopeBasedSyncService; this.formMappingService = formMappingService; this.entityApprovalStatusService = entityApprovalStatusService; + this.txDataControllerHelper = txDataControllerHelper; } @RequestMapping(value = "/programEnrolments", method = RequestMethod.POST) @@ -76,6 +76,7 @@ public AvniEntityResponse save(@RequestBody ProgramEnrolmentRequest request) { @Transactional public AvniEntityResponse saveForWeb(@RequestBody ProgramEnrolmentRequest request) { ProgramEnrolment programEnrolment = programEnrolmentService.programEnrolmentSave(request); + txDataControllerHelper.checkSubjectAccess(programEnrolment.getIndividual()); //Assuming that EnrollmentDetails will not be edited when exited FormMapping formMapping = programEnrolmentService.getFormMapping(programEnrolment); @@ -159,6 +160,7 @@ public ResponseEntity voidProgramEnrolment(@PathVariable String uuid) { if (programEnrolment == null) { return ResponseEntity.notFound().build(); } + txDataControllerHelper.checkSubjectAccess(programEnrolment.getIndividual()); programEnrolmentService.voidEnrolment(programEnrolment); return ResponseEntity.ok().build(); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/TxDataControllerHelper.java b/avni-server-api/src/main/java/org/avni/server/web/TxDataControllerHelper.java new file mode 100644 index 000000000..da9a456d4 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/web/TxDataControllerHelper.java @@ -0,0 +1,28 @@ +package org.avni.server.web; + +import org.avni.server.domain.Individual; +import org.avni.server.domain.accessControl.SubjectPartitionCheckStatus; +import org.avni.server.domain.accessControl.SubjectPartitionData; +import org.avni.server.service.accessControl.AccessControlService; +import org.avni.server.util.BadRequestError; +import org.springframework.stereotype.Component; + +@Component +public class TxDataControllerHelper { + private final AccessControlService accessControlService; + + public TxDataControllerHelper(AccessControlService accessControlService) { + this.accessControlService = accessControlService; + } + + public void checkSubjectAccess(Individual subject, SubjectPartitionData subjectPartitionData) { + SubjectPartitionCheckStatus subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, subjectPartitionData); + if (!subjectPartitionCheckStatus.isPassed()) { + throw new BadRequestError(subjectPartitionCheckStatus.getMessage()); + } + } + + public void checkSubjectAccess(Individual subject) { + this.checkSubjectAccess(subject, SubjectPartitionData.create(subject)); + } +} diff --git a/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceStub.java b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceStub.java index 33a2602fb..44db7ad2d 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceStub.java +++ b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceStub.java @@ -5,7 +5,7 @@ public class AccessControlServiceStub extends AccessControlService { public AccessControlServiceStub() { - super(null, null, null, null, null, null, null); + super(null, null, null, null, null, null, null, null); } @Override diff --git a/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceTest.java index 7a16fa2f8..1b66598de 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceTest.java @@ -27,7 +27,7 @@ public void setup() { @Test public void adminHasPrivilegeForNonTxn() { - AccessControlService accessControlService = new AccessControlService(userRepository, null, null, null, privilegeRepository, null, null); + AccessControlService accessControlService = new AccessControlService(userRepository, null, null, null, privilegeRepository, null, null, null); User user = new UserBuilder().id(1L).isAdmin(true).build(); when(privilegeRepository.isAllowedForAdmin(PrivilegeType.EditSubjectType)).thenReturn(true); accessControlService.checkPrivilege(user, PrivilegeType.EditSubjectType); @@ -36,7 +36,7 @@ public void adminHasPrivilegeForNonTxn() { @Test(expected = AvniAccessException.class) public void adminDoesntHavePrivilegeTxn() { initMocks(this); - AccessControlService accessControlService = new AccessControlService(userRepository, null, null, null, privilegeRepository, null, null); + AccessControlService accessControlService = new AccessControlService(userRepository, null, null, null, privilegeRepository, null, null, null); User user = new UserBuilder().id(1L).isAdmin(true).build(); when(privilegeRepository.isAllowedForAdmin(PrivilegeType.EditSubject)).thenReturn(false); accessControlService.checkPrivilege(user, PrivilegeType.EditSubject); From 41c4f440851b30eefbf4508fa31a5f2b6ddbb962 Mon Sep 17 00:00:00 2001 From: Joy A Date: Mon, 5 Aug 2024 15:23:00 +0530 Subject: [PATCH 059/129] #758 | UserAndCatchmentWriter - sync header names from sample to writer Ignore metadata row if present in the uploaded file --- .../batch/csv/writer/UserAndCatchmentWriter.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index 86c443fdd..6a2afb778 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -34,6 +34,7 @@ public class UserAndCatchmentWriter implements ItemWriter, Serializable { private final ConceptService conceptService; private final Pattern compoundHeaderPattern; private final ResetSyncService resetSyncService; + private final String METADATA_ROW_START_STRING = "Mandatory field."; @Autowired public UserAndCatchmentWriter(CatchmentService catchmentService, @@ -62,17 +63,18 @@ public void write(List rows) throws Exception { private void write(Row row) throws Exception { String fullAddress = row.get("Location with full hierarchy"); + if (fullAddress != null && fullAddress.startsWith(METADATA_ROW_START_STRING)) return; String catchmentName = row.get("Catchment Name"); String nameOfUser = row.get("Full Name of User"); String username = row.get("Username"); - String email = row.get("Email"); - String phoneNumber = row.get("Phone"); - String language = row.get("Language"); + String email = row.get("Email Address"); + String phoneNumber = row.get("Mobile Number"); + String language = row.get("Preferred Language"); Locale locale = S.isEmpty(language) ? Locale.en : Locale.valueByName(language); Boolean trackLocation = row.getBool("Track Location"); String datePickerMode = row.get("Date picker mode"); Boolean beneficiaryMode = row.getBool("Enable Beneficiary mode"); - String idPrefix = row.get("Beneficiary ID Prefix"); + String idPrefix = row.get("Identifier Prefix"); String groupsSpecified = row.get("User Groups"); JsonObject syncSettings = constructSyncSettings(row); From c42ec9bab6d872fe8d74ce2d5813317281d849b7 Mon Sep 17 00:00:00 2001 From: himeshr Date: Mon, 5 Aug 2024 18:11:11 +0530 Subject: [PATCH 060/129] avniproject/avni-webapp#1293 | include isVoided info in toString --- .../src/main/java/org/avni/server/web/request/CHSRequest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java index 27d98afe8..b4e7f8c19 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/CHSRequest.java @@ -68,6 +68,7 @@ public String toString() { sb.append("uuid='").append(uuid).append('\''); if (id != null && id != 0) sb.append(", id=").append(id); + sb.append("isVoided ='").append(isVoided).append('\''); sb.append('}'); return sb.toString(); } From 403e214b3855208aa9e5677698dcf19442d33bf2 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 6 Aug 2024 13:15:52 +0530 Subject: [PATCH 061/129] avniproject/avni-webapp#1298 - test for verifying various scenarios --- .../org/avni/server/domain/SubjectType.java | 4 + .../SubjectPartitionCheckStatus.java | 1 - .../accessControl/AccessControlService.java | 7 +- .../avni/server/web/IndividualController.java | 1 + .../domain/factory/txn/SubjectBuilder.java | 5 + .../AccessControlServiceIntegrationTest.java | 221 ++++++++++++++++++ .../service/builder/TestDataSetupService.java | 35 +++ 7 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceIntegrationTest.java diff --git a/avni-server-api/src/main/java/org/avni/server/domain/SubjectType.java b/avni-server-api/src/main/java/org/avni/server/domain/SubjectType.java index 56342d19c..180bd001b 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/SubjectType.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/SubjectType.java @@ -333,6 +333,10 @@ public Boolean isSyncRegistrationConcept2Usable() { return isSyncRegistrationConcept2Usable; } + public boolean isAnySyncRegistrationConceptUsable() { + return (isSyncRegistrationConcept1Usable != null && isSyncRegistrationConcept1Usable()) || (isSyncRegistrationConcept2Usable != null && isSyncRegistrationConcept2Usable()); + } + public void setSyncRegistrationConcept2Usable(Boolean syncRegistrationConcept2Usable) { isSyncRegistrationConcept2Usable = syncRegistrationConcept2Usable; } diff --git a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java index b4f45e70a..6810d16d0 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/accessControl/SubjectPartitionCheckStatus.java @@ -3,7 +3,6 @@ public class SubjectPartitionCheckStatus { public static final String NotDirectlyAssignedToThisUser = "notDirectlyAssignedToThisUser"; public static final String NotInThisUsersCatchment = "notInThisUsersCatchment"; - public static final String SubjectTypeNotConfigured = "subjectTypeNotConfigured"; public static final String UserSyncAttributeNotConfigured = "userSyncAttributeNotConfigured"; public static final String SyncAttributeForUserNotValidForUpdate = "syncAttributeForUserNotValidForUpdate"; diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java index f4f66d8fc..84e258f7f 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java @@ -235,12 +235,7 @@ public SubjectPartitionCheckStatus checkSubjectAccess(Individual subject, Subjec return SubjectPartitionCheckStatus.failed(SubjectPartitionCheckStatus.NotDirectlyAssignedToThisUser); } - boolean checkByObservationValue = !subjectType.isShouldSyncByLocation() && !subjectType.isDirectlyAssignable(); - if (checkByObservationValue) { - if (subjectType.getSyncRegistrationConcept1() == null) { - return SubjectPartitionCheckStatus.failed(SubjectPartitionCheckStatus.SubjectTypeNotConfigured); - } - + if (subjectType.isAnySyncRegistrationConceptUsable()) { List syncSettingsList = currentUser.getSyncSettingsList(); UserSyncSettings userSyncSettingsForSubjectType = syncSettingsList.stream().filter(userSyncSettings -> userSyncSettings.getSubjectTypeUUID().equals(subjectType.getUuid())).findFirst().orElse(null); if (userSyncSettingsForSubjectType == null) { diff --git a/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java b/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java index 1987b2570..ce2b062b6 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java @@ -354,6 +354,7 @@ public AvniEntityResponse saveForWeb(@RequestBody IndividualRequest individualRe FormMapping formMapping = formMappingService.findBy(individual.getSubjectType(), null, null, FormType.IndividualProfile); entityApprovalStatusService.createStatus(EntityApprovalStatus.EntityType.Subject, individual.getId(), ApprovalStatus.Status.Pending, individual.getSubjectType().getUuid(), formMapping); + // Sync attribute values are picked from the field on individual and not from observations, hence this should be done after the individual is saved txDataControllerHelper.checkSubjectAccess(individual, subjectPartitionData); logger.info(String.format("Saved individual with UUID %s", individualRequest.getUuid())); diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/txn/SubjectBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/txn/SubjectBuilder.java index ac1d8cb92..8efc12d84 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/txn/SubjectBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/txn/SubjectBuilder.java @@ -67,4 +67,9 @@ public SubjectBuilder withMandatoryFieldsForNewEntity() { String s = UUID.randomUUID().toString(); return withUUID(s).withFirstName(s).withRegistrationDate(LocalDate.now()).withObservations(new ObservationCollection()); } + + public SubjectBuilder withSyncConcept1Value(Object value) { + individual.setSyncConcept1Value(value.toString()); + return this; + } } diff --git a/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceIntegrationTest.java new file mode 100644 index 000000000..bad5874f1 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/service/accessControl/AccessControlServiceIntegrationTest.java @@ -0,0 +1,221 @@ +package org.avni.server.service.accessControl; + +import org.avni.server.common.AbstractControllerIntegrationTest; +import org.avni.server.domain.*; +import org.avni.server.domain.accessControl.SubjectPartitionCheckStatus; +import org.avni.server.domain.accessControl.SubjectPartitionData; +import org.avni.server.domain.factory.AddressLevelBuilder; +import org.avni.server.domain.factory.TestUserSyncSettingsBuilder; +import org.avni.server.domain.factory.UserBuilder; +import org.avni.server.domain.factory.txn.SubjectBuilder; +import org.avni.server.domain.metadata.SubjectTypeBuilder; +import org.avni.server.service.UserSubjectAssignmentService; +import org.avni.server.service.builder.*; +import org.avni.server.web.request.syncAttribute.UserSyncSettings; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +import java.util.Collections; + +import static org.junit.Assert.*; + +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +public class AccessControlServiceIntegrationTest extends AbstractControllerIntegrationTest { + @Autowired + private TestDataSetupService testDataSetupService; + @Autowired + private TestGroupService testGroupService; + @Autowired + private TestSubjectTypeService testSubjectTypeService; + @Autowired + private TestProgramService testProgramService; + @Autowired + private TestSubjectService testSubjectService; + @Autowired + private AccessControlService accessControlService; + @Autowired + private TestLocationService testLocationService; + @Autowired + private UserSubjectAssignmentService userSubjectAssignmentService; + + private TestDataSetupService.TestOrganisationData organisationData; + private TestDataSetupService.TestCatchmentData catchmentData; + + @Override + public void setUp() throws Exception { + super.setUp(); + organisationData = testDataSetupService.setupOrganisation(); + catchmentData = testDataSetupService.setupACatchment(); + } + + @Test + public void checkSubjectAccessForCatchmentTypePartition() { + User user = organisationData.getUser(); + user.setCatchment(catchmentData.getCatchment()); + userRepository.save(user); + setUser(user); + + AddressLevel outsideCatchment = testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().type(catchmentData.getAddressLevelType()).build()); + SubjectType subjectType = testSubjectTypeService.createWithDefaults( + new SubjectTypeBuilder() + .setShouldSyncByLocation(true) + .setMandatoryFieldsForNewEntity() + .setUuid("subjectType") + .setName("subjectType") + .build()); + + // new subject + Individual subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectType) + .withLocation(catchmentData.getAddressLevel1()).build(); + SubjectPartitionCheckStatus subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, null); + assertTrue(subjectPartitionCheckStatus.getMessage(), subjectPartitionCheckStatus.isPassed()); + + // existing subject + subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectType) + .withLocation(catchmentData.getAddressLevel1()).build(); + subject = testSubjectService.save(subject); + SubjectPartitionData previousPartitionState = SubjectPartitionData.create(subject); + subject.setAddressLevel(catchmentData.getAddressLevel2()); + subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, previousPartitionState); + assertTrue(subjectPartitionCheckStatus.getMessage(), subjectPartitionCheckStatus.isPassed()); + + // new subject, outside catchment + subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectType) + .withLocation(outsideCatchment).build(); + subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, null); + assertFalse(subjectPartitionCheckStatus.isPassed()); + assertEquals(SubjectPartitionCheckStatus.NotInThisUsersCatchment, subjectPartitionCheckStatus.getMessage()); + + + // existing subject, outside catchment + subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectType) + .withLocation(outsideCatchment).build(); + subject = testSubjectService.save(subject); + previousPartitionState = SubjectPartitionData.create(subject); + + subject.setAddressLevel(catchmentData.getAddressLevel2()); + subject = testSubjectService.save(subject); + subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, previousPartitionState); + assertFalse(subjectPartitionCheckStatus.isPassed()); + assertEquals(SubjectPartitionCheckStatus.NotInThisUsersCatchment, subjectPartitionCheckStatus.getMessage()); + // + } + + @Test + public void checkSubjectAccessForSyncAttributes() { + User user = organisationData.getUser(); + TestDataSetupService.TestSyncAttributeBasedSubjectTypeData subjectTypeData = testDataSetupService.setupSubjectTypeWithSyncAttributes(); + UserSyncSettings userSyncSettings = new TestUserSyncSettingsBuilder() + .setSubjectTypeUUID(subjectTypeData.getSubjectType().getUuid()) + .setSyncConcept1(subjectTypeData.getSyncConcept().getUuid()) + .setSyncConcept1Values(Collections.singletonList(subjectTypeData.getSyncConcept().getAnswerConcept("Answer 1").getUuid())) + .build(); + userRepository.save(new UserBuilder(user) + .withCatchment(catchmentData.getCatchment()) + .withOperatingIndividualScope(OperatingIndividualScope.ByCatchment) + .withSubjectTypeSyncSettings(userSyncSettings).build()); + setUser(user); + + // new subject + Individual subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectTypeData.getSubjectType()) + .withLocation(catchmentData.getAddressLevel1()) + .withSyncConcept1Value(subjectTypeData.getSyncConcept().getAnswerConcept("Answer 1").getUuid()) + .build(); + SubjectPartitionCheckStatus subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, null); + assertTrue(subjectPartitionCheckStatus.getMessage(), subjectPartitionCheckStatus.isPassed()); + + // new subject, with not matching sync attribute value + subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectTypeData.getSubjectType()) + .withLocation(catchmentData.getAddressLevel1()) + .withSyncConcept1Value(subjectTypeData.getSyncConcept().getAnswerConcept("Answer 2").getUuid()) + .build(); + subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, null); + assertFalse(subjectPartitionCheckStatus.isPassed()); + assertEquals(SubjectPartitionCheckStatus.SyncAttributeForUserNotValidForUpdate, subjectPartitionCheckStatus.getMessage()); + + + // existing subject with matching sync attribute value + ObservationCollection observationCollection = new ObservationCollection(); + observationCollection.put(subjectTypeData.getSyncConcept().getUuid(), subjectTypeData.getSyncConcept().getAnswerConcept("Answer 1").getUuid()); + subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectTypeData.getSubjectType()) + .withLocation(catchmentData.getAddressLevel1()) + .withObservations(observationCollection) + .build(); + subject = testSubjectService.save(subject); + SubjectPartitionData previousPartitionState = SubjectPartitionData.create(subject); + + subject.setSyncConcept1Value(subjectTypeData.getSyncConcept().getAnswerConcept("Answer 2").getUuid()); + subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, previousPartitionState); + assertTrue(subjectPartitionCheckStatus.getMessage(), subjectPartitionCheckStatus.isPassed()); + + + // existing subject with not matching sync attribute value + observationCollection = new ObservationCollection(); + observationCollection.put(subjectTypeData.getSyncConcept().getUuid(), subjectTypeData.getSyncConcept().getAnswerConcept("Answer 2").getUuid()); + subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectTypeData.getSubjectType()) + .withLocation(catchmentData.getAddressLevel1()) + .build(); + subject = testSubjectService.save(subject); + previousPartitionState = SubjectPartitionData.create(subject); + + subject.setSyncConcept1Value(subjectTypeData.getSyncConcept().getAnswerConcept("Answer 1").getUuid()); + subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, previousPartitionState); + assertFalse(subjectPartitionCheckStatus.isPassed()); + assertEquals(SubjectPartitionCheckStatus.SyncAttributeForUserNotValidForUpdate, subjectPartitionCheckStatus.getMessage()); + } + + @Test + public void checkForDirectAssignment() throws ValidationException { + User user = organisationData.getUser(); + userRepository.save(new UserBuilder(user) + .withCatchment(catchmentData.getCatchment()) + .withOperatingIndividualScope(OperatingIndividualScope.ByCatchment).build()); + setUser(user); + userRepository.save(new UserBuilder(organisationData.getUser2()) + .withCatchment(catchmentData.getCatchment()) + .withOperatingIndividualScope(OperatingIndividualScope.ByCatchment).build()); + + SubjectType subjectType = testSubjectTypeService.createWithDefaults( + new SubjectTypeBuilder() + .setMandatoryFieldsForNewEntity() + .setUuid("subjectType") + .setName("subjectType") + .setDirectlyAssignable(true) + .build()); + + + // existing subject assigned to current user + Individual subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectType) + .withLocation(catchmentData.getAddressLevel1()).build(); + subject = testSubjectService.save(subject); + userSubjectAssignmentService.assignSubjects(user, Collections.singletonList(subject), false); + SubjectPartitionData previousPartitionState = SubjectPartitionData.create(subject); + + SubjectPartitionCheckStatus subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, previousPartitionState); + assertTrue(subjectPartitionCheckStatus.getMessage(), subjectPartitionCheckStatus.isPassed()); + + + // existing subject, outside catchment + subject = new SubjectBuilder().withMandatoryFieldsForNewEntity() + .withSubjectType(subjectType) + .withLocation(catchmentData.getAddressLevel1()).build(); + subject = testSubjectService.save(subject); + userSubjectAssignmentService.assignSubjects(organisationData.getUser2(), Collections.singletonList(subject), false); + previousPartitionState = SubjectPartitionData.create(subject); + + subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, previousPartitionState); + assertFalse(subjectPartitionCheckStatus.isPassed()); + assertEquals(SubjectPartitionCheckStatus.NotDirectlyAssignedToThisUser, subjectPartitionCheckStatus.getMessage()); + } +} diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java index 58d556696..7dcf06667 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java @@ -5,6 +5,7 @@ import org.avni.server.domain.factory.*; import org.avni.server.domain.factory.access.TestGroupBuilder; import org.avni.server.domain.factory.access.TestUserGroupBuilder; +import org.avni.server.domain.metadata.SubjectTypeBuilder; import org.avni.server.web.TestWebContextService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -31,6 +32,10 @@ public class TestDataSetupService { private TestLocationService testLocationService; @Autowired private TestCatchmentService testCatchmentService; + @Autowired + private TestConceptService testConceptService; + @Autowired + private TestSubjectTypeService testSubjectTypeService; public TestOrganisationData setupOrganisation(String orgSuffix) { Group group = new TestGroupBuilder().withMandatoryFieldsForNewEntity().build(); @@ -66,6 +71,36 @@ public TestCatchmentData setupACatchment() { return new TestCatchmentData(addressLevelType, addressLevel1, addressLevel2, catchment); } + public TestSyncAttributeBasedSubjectTypeData setupSubjectTypeWithSyncAttributes() { + Concept conceptForAttributeBasedSync = testConceptService.createCodedConcept("Concept Name 1", "Answer 1", "Answer 2"); + SubjectType subjectType = testSubjectTypeService.createWithDefaults( + new SubjectTypeBuilder() + .setMandatoryFieldsForNewEntity() + .setUuid("subjectTypeWithSyncAttributeBasedSync") + .setName("subjectTypeWithSyncAttributeBasedSync") + .setSyncRegistrationConcept1Usable(true) + .setSyncRegistrationConcept1(conceptForAttributeBasedSync.getUuid()).build()); + return new TestSyncAttributeBasedSubjectTypeData(subjectType, conceptForAttributeBasedSync); + } + + public static class TestSyncAttributeBasedSubjectTypeData { + private final SubjectType subjectType; + private final Concept conceptForAttributeBasedSync; + + public TestSyncAttributeBasedSubjectTypeData(SubjectType subjectType, Concept conceptForAttributeBasedSync) { + this.subjectType = subjectType; + this.conceptForAttributeBasedSync = conceptForAttributeBasedSync; + } + + public SubjectType getSubjectType() { + return subjectType; + } + + public Concept getSyncConcept() { + return conceptForAttributeBasedSync; + } + } + public static class TestCatchmentData { private final AddressLevelType addressLevelType; private final AddressLevel addressLevel1; From cda4667754cd794a91fd1230359e821bd41efaed Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 6 Aug 2024 16:37:53 +0530 Subject: [PATCH 062/129] avniproject/avni-webapp#1298 - return error response and rollback transaction on error --- .../org/avni/server/util/BadRequestError.java | 2 - .../avni/server/web/EncounterController.java | 46 +++++++++------ .../avni/server/web/IndividualController.java | 56 +++++++++++-------- .../web/ProgramEncounterController.java | 45 +++++++++------ .../web/ProgramEnrolmentController.java | 41 +++++++++----- .../server/web/TxDataControllerHelper.java | 13 +++-- .../web/response/AvniEntityResponse.java | 21 ++++++- 7 files changed, 146 insertions(+), 78 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/util/BadRequestError.java b/avni-server-api/src/main/java/org/avni/server/util/BadRequestError.java index a89b12740..116573de4 100644 --- a/avni-server-api/src/main/java/org/avni/server/util/BadRequestError.java +++ b/avni-server-api/src/main/java/org/avni/server/util/BadRequestError.java @@ -5,9 +5,7 @@ Returning HTTP 400 Bad Request on throw of this exception is handled by org.avni.web.ErrorInterceptors. */ public class BadRequestError extends RuntimeException { - public BadRequestError(String format, Object... args) { super(String.format(format, args)); } - } diff --git a/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java b/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java index c9d493cfc..18f07d9c0 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java @@ -18,6 +18,7 @@ import org.avni.server.web.request.PointRequest; import org.avni.server.web.request.rules.RulesContractWrapper.Decision; import org.avni.server.web.request.rules.RulesContractWrapper.Decisions; +import org.avni.server.web.response.AvniEntityResponse; import org.avni.server.web.response.slice.SlicedResources; import org.joda.time.DateTime; import org.slf4j.LoggerFactory; @@ -31,6 +32,7 @@ import org.springframework.hateoas.Resource; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import org.springframework.web.bind.annotation.*; import javax.transaction.Transactional; @@ -112,15 +114,18 @@ public void save(@RequestBody EncounterRequest request) { @RequestMapping(value = "/web/encounters", method = RequestMethod.POST) @Transactional @PreAuthorize(value = "hasAnyAuthority('user')") - public void saveForWeb(@RequestBody EncounterRequest request) { - logger.info("Saving encounter with uuid {}}", request.getUuid()); - - Encounter encounter = createEncounter(request); - if (encounter != null) // create encounter method needs fixing. it should not return null in any case - txDataControllerHelper.checkSubjectAccess(encounter.getIndividual()); - addEntityApprovalStatusIfRequired(encounter); - - logger.info(String.format("Saved encounter with uuid %s", request.getUuid())); + public AvniEntityResponse saveForWeb(@RequestBody EncounterRequest request) { + try { + logger.info("Saving encounter with uuid {}}", request.getUuid()); + Encounter encounter = createEncounter(request); + if (encounter != null) // create encounter method needs fixing. it should not return null in any case + txDataControllerHelper.checkSubjectAccess(encounter.getIndividual()); + addEntityApprovalStatusIfRequired(encounter); + logger.info(String.format("Saved encounter with uuid %s", request.getUuid())); + return new AvniEntityResponse(encounter); + } catch (TxDataControllerHelper.TxDataPartitionAccessDeniedException e) { + return AvniEntityResponse.error(e.getMessage()); + } } private void addEntityApprovalStatusIfRequired(Encounter encounter) { @@ -250,16 +255,21 @@ public PagedResources> getEncountersByOperatingIndividualSco @PreAuthorize(value = "hasAnyAuthority('user')") @ResponseBody @Transactional - public ResponseEntity voidEncounter(@PathVariable String uuid) { - Encounter encounter = encounterRepository.findByUuid(uuid); - if (encounter == null) { - return ResponseEntity.notFound().build(); + public AvniEntityResponse voidEncounter(@PathVariable String uuid) { + try { + Encounter encounter = encounterRepository.findByUuid(uuid); + if (encounter == null) { + return AvniEntityResponse.error("Encounter not found"); + } + txDataControllerHelper.checkSubjectAccess(encounter.getIndividual()); + accessControlService.checkEncounterPrivilege(PrivilegeType.VoidVisit, encounter); + encounter.setVoided(true); + encounterService.save(encounter); + return new AvniEntityResponse(encounter); + } catch (TxDataControllerHelper.TxDataPartitionAccessDeniedException e) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return AvniEntityResponse.error(e.getMessage()); } - txDataControllerHelper.checkSubjectAccess(encounter.getIndividual()); - accessControlService.checkEncounterPrivilege(PrivilegeType.VoidVisit, encounter); - encounter.setVoided(true); - encounterService.save(encounter); - return ResponseEntity.ok().build(); } @Override diff --git a/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java b/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java index ce2b062b6..eae084d01 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java @@ -13,6 +13,7 @@ import org.avni.server.projection.IndividualWebProjection; import org.avni.server.service.*; import org.avni.server.service.accessControl.AccessControlService; +import org.avni.server.util.BadRequestError; import org.avni.server.web.request.EncounterContract; import org.avni.server.web.request.IndividualRequest; import org.avni.server.web.request.PointRequest; @@ -39,6 +40,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import org.springframework.web.bind.annotation.*; import javax.transaction.Transactional; @@ -264,15 +266,20 @@ public Page getAllCompletedEncounters( @DeleteMapping("/web/subject/{uuid}") @ResponseBody @Transactional - public ResponseEntity voidSubject(@PathVariable String uuid) { - Individual individual = individualRepository.findByUuid(uuid); - txDataControllerHelper.checkSubjectAccess(individual); - if (individual == null) { - return ResponseEntity.notFound().build(); + public AvniEntityResponse voidSubject(@PathVariable String uuid) { + try { + Individual individual = individualRepository.findByUuid(uuid); + txDataControllerHelper.checkSubjectAccess(individual); + if (individual == null) { + return AvniEntityResponse.error("Subject not found"); + } + accessControlService.checkSubjectPrivilege(PrivilegeType.VoidSubject, individual); + individualService.voidSubject(individual); + return new AvniEntityResponse(individual); + } catch (TxDataControllerHelper.TxDataPartitionAccessDeniedException | BadRequestError e) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return AvniEntityResponse.error(e.getMessage()); } - accessControlService.checkSubjectPrivilege(PrivilegeType.VoidSubject, individual); - individualService.voidSubject(individual); - return ResponseEntity.ok().build(); } @GetMapping("/subject/search") @@ -345,20 +352,25 @@ public Resource process(Resource resource) { @Transactional @PreAuthorize(value = "hasAnyAuthority('user')") public AvniEntityResponse saveForWeb(@RequestBody IndividualRequest individualRequest) { - logger.info(String.format("Saving individual with UUID %s", individualRequest.getUuid())); - Individual savedIndividual = individualService.getIndividual(individualRequest.getUuid()); - //Subject is changed after this line, hence the following line cannot be moved down closer to its usage - SubjectPartitionData subjectPartitionData = SubjectPartitionData.create(savedIndividual); - - Individual individual = createIndividual(individualRequest); - - FormMapping formMapping = formMappingService.findBy(individual.getSubjectType(), null, null, FormType.IndividualProfile); - entityApprovalStatusService.createStatus(EntityApprovalStatus.EntityType.Subject, individual.getId(), ApprovalStatus.Status.Pending, individual.getSubjectType().getUuid(), formMapping); - // Sync attribute values are picked from the field on individual and not from observations, hence this should be done after the individual is saved - txDataControllerHelper.checkSubjectAccess(individual, subjectPartitionData); - logger.info(String.format("Saved individual with UUID %s", individualRequest.getUuid())); - - return new AvniEntityResponse(individual); + try { + logger.info(String.format("Saving individual with UUID %s", individualRequest.getUuid())); + Individual savedIndividual = individualService.getIndividual(individualRequest.getUuid()); + //Subject is changed after this line, hence the following line cannot be moved down closer to its usage + SubjectPartitionData subjectPartitionData = SubjectPartitionData.create(savedIndividual); + + Individual individual = createIndividual(individualRequest); + + FormMapping formMapping = formMappingService.findBy(individual.getSubjectType(), null, null, FormType.IndividualProfile); + entityApprovalStatusService.createStatus(EntityApprovalStatus.EntityType.Subject, individual.getId(), ApprovalStatus.Status.Pending, individual.getSubjectType().getUuid(), formMapping); + // Sync attribute values are picked from the field on individual and not from observations, hence this should be done after the individual is saved + txDataControllerHelper.checkSubjectAccess(individual, subjectPartitionData); + logger.info(String.format("Saved individual with UUID %s", individualRequest.getUuid())); + + return new AvniEntityResponse(individual); + } catch (TxDataControllerHelper.TxDataPartitionAccessDeniedException e) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return AvniEntityResponse.error(e.getMessage()); + } } private Individual createIndividual(IndividualRequest individualRequest) { diff --git a/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java b/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java index 22dc2ed03..4ba2678eb 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java @@ -11,6 +11,7 @@ import org.avni.server.service.accessControl.AccessControlService; import org.avni.server.web.request.ProgramEncounterContract; import org.avni.server.web.request.ProgramEncounterRequest; +import org.avni.server.web.response.AvniEntityResponse; import org.avni.server.web.response.slice.SlicedResources; import org.joda.time.DateTime; import org.slf4j.LoggerFactory; @@ -24,6 +25,7 @@ import org.springframework.hateoas.Resource; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import org.springframework.web.bind.annotation.*; import javax.transaction.Transactional; @@ -82,15 +84,21 @@ public void save(@RequestBody ProgramEncounterRequest request) { @RequestMapping(value = "/web/programEncounters", method = RequestMethod.POST) @Transactional @PreAuthorize(value = "hasAnyAuthority('user')") - public void saveForWeb(@RequestBody ProgramEncounterRequest request) { - ProgramEncounter programEncounter = programEncounterService.saveProgramEncounter(request); - txDataControllerHelper.checkSubjectAccess(programEncounter.getProgramEnrolment().getIndividual()); - if (request.getVisitSchedules() != null && !request.getVisitSchedules().isEmpty()) { - programEncounterService.saveVisitSchedules(request.getProgramEnrolmentUUID(), request.getVisitSchedules(), request.getUuid()); + public AvniEntityResponse saveForWeb(@RequestBody ProgramEncounterRequest request) { + try { + ProgramEncounter programEncounter = programEncounterService.saveProgramEncounter(request); + txDataControllerHelper.checkSubjectAccess(programEncounter.getProgramEnrolment().getIndividual()); + if (request.getVisitSchedules() != null && !request.getVisitSchedules().isEmpty()) { + programEncounterService.saveVisitSchedules(request.getProgramEnrolmentUUID(), request.getVisitSchedules(), request.getUuid()); + } + + FormMapping formMapping = programEncounterService.getFormMapping(programEncounter); + entityApprovalStatusService.createStatus(EntityApprovalStatus.EntityType.ProgramEncounter, programEncounter.getId(), ApprovalStatus.Status.Pending, programEncounter.getEncounterType().getUuid(), formMapping); + return new AvniEntityResponse(programEncounter); + } catch (TxDataControllerHelper.TxDataPartitionAccessDeniedException e) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return AvniEntityResponse.error(e.getMessage()); } - - FormMapping formMapping = programEncounterService.getFormMapping(programEncounter); - entityApprovalStatusService.createStatus(EntityApprovalStatus.EntityType.ProgramEncounter, programEncounter.getId(), ApprovalStatus.Status.Pending, programEncounter.getEncounterType().getUuid(), formMapping); } @RequestMapping(value = "/programEncounter/search/byIndividualsOfCatchmentAndLastModified", method = RequestMethod.GET) @@ -151,15 +159,20 @@ public PagedResources> getProgramEncountersByOperatin @PreAuthorize(value = "hasAnyAuthority('user')") @ResponseBody @Transactional - public ResponseEntity voidProgramEncounter(@PathVariable String uuid) { - ProgramEncounter programEncounter = programEncounterRepository.findByUuid(uuid); - if (programEncounter == null) { - return ResponseEntity.notFound().build(); + public AvniEntityResponse voidProgramEncounter(@PathVariable String uuid) { + try { + ProgramEncounter programEncounter = programEncounterRepository.findByUuid(uuid); + if (programEncounter == null) { + return AvniEntityResponse.error("Program Encounter not found"); + } + txDataControllerHelper.checkSubjectAccess(programEncounter.getProgramEnrolment().getIndividual()); + programEncounter.setVoided(true); + programEncounterService.save(programEncounter); + return new AvniEntityResponse(programEncounter); + } catch (TxDataControllerHelper.TxDataPartitionAccessDeniedException e) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return AvniEntityResponse.error(e.getMessage()); } - txDataControllerHelper.checkSubjectAccess(programEncounter.getProgramEnrolment().getIndividual()); - programEncounter.setVoided(true); - programEncounterService.save(programEncounter); - return ResponseEntity.ok().build(); } @Override diff --git a/avni-server-api/src/main/java/org/avni/server/web/ProgramEnrolmentController.java b/avni-server-api/src/main/java/org/avni/server/web/ProgramEnrolmentController.java index f5a2f65a9..418827037 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ProgramEnrolmentController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ProgramEnrolmentController.java @@ -30,6 +30,7 @@ import org.springframework.hateoas.Resource; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import org.springframework.web.bind.annotation.*; import javax.transaction.Transactional; @@ -75,14 +76,19 @@ public AvniEntityResponse save(@RequestBody ProgramEnrolmentRequest request) { @PreAuthorize(value = "hasAnyAuthority('user')") @Transactional public AvniEntityResponse saveForWeb(@RequestBody ProgramEnrolmentRequest request) { - ProgramEnrolment programEnrolment = programEnrolmentService.programEnrolmentSave(request); - txDataControllerHelper.checkSubjectAccess(programEnrolment.getIndividual()); - - //Assuming that EnrollmentDetails will not be edited when exited - FormMapping formMapping = programEnrolmentService.getFormMapping(programEnrolment); - entityApprovalStatusService.createStatus(EntityApprovalStatus.EntityType.ProgramEnrolment, programEnrolment.getId(), ApprovalStatus.Status.Pending, programEnrolment.getProgram().getUuid(), formMapping); - - return new AvniEntityResponse(programEnrolment); + try { + ProgramEnrolment programEnrolment = programEnrolmentService.programEnrolmentSave(request); + txDataControllerHelper.checkSubjectAccess(programEnrolment.getIndividual()); + + //Assuming that EnrollmentDetails will not be edited when exited + FormMapping formMapping = programEnrolmentService.getFormMapping(programEnrolment); + entityApprovalStatusService.createStatus(EntityApprovalStatus.EntityType.ProgramEnrolment, programEnrolment.getId(), ApprovalStatus.Status.Pending, programEnrolment.getProgram().getUuid(), formMapping); + + return new AvniEntityResponse(programEnrolment); + } catch (TxDataControllerHelper.TxDataPartitionAccessDeniedException e) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return AvniEntityResponse.error(e.getMessage()); + } } @GetMapping(value = {"/programEnrolment/v2"}) @@ -155,14 +161,19 @@ public Page getAllCompletedEncounters( @PreAuthorize(value = "hasAnyAuthority('user')") @ResponseBody @Transactional - public ResponseEntity voidProgramEnrolment(@PathVariable String uuid) { - ProgramEnrolment programEnrolment = programEnrolmentRepository.findByUuid(uuid); - if (programEnrolment == null) { - return ResponseEntity.notFound().build(); + public AvniEntityResponse voidProgramEnrolment(@PathVariable String uuid) { + try { + ProgramEnrolment programEnrolment = programEnrolmentRepository.findByUuid(uuid); + if (programEnrolment == null) { + return AvniEntityResponse.error("Program Enrolment not found"); + } + txDataControllerHelper.checkSubjectAccess(programEnrolment.getIndividual()); + programEnrolmentService.voidEnrolment(programEnrolment); + return new AvniEntityResponse(programEnrolment); + } catch (TxDataControllerHelper.TxDataPartitionAccessDeniedException e) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return AvniEntityResponse.error(e.getMessage()); } - txDataControllerHelper.checkSubjectAccess(programEnrolment.getIndividual()); - programEnrolmentService.voidEnrolment(programEnrolment); - return ResponseEntity.ok().build(); } @Override diff --git a/avni-server-api/src/main/java/org/avni/server/web/TxDataControllerHelper.java b/avni-server-api/src/main/java/org/avni/server/web/TxDataControllerHelper.java index da9a456d4..f1436d3e9 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/TxDataControllerHelper.java +++ b/avni-server-api/src/main/java/org/avni/server/web/TxDataControllerHelper.java @@ -4,7 +4,6 @@ import org.avni.server.domain.accessControl.SubjectPartitionCheckStatus; import org.avni.server.domain.accessControl.SubjectPartitionData; import org.avni.server.service.accessControl.AccessControlService; -import org.avni.server.util.BadRequestError; import org.springframework.stereotype.Component; @Component @@ -15,14 +14,20 @@ public TxDataControllerHelper(AccessControlService accessControlService) { this.accessControlService = accessControlService; } - public void checkSubjectAccess(Individual subject, SubjectPartitionData subjectPartitionData) { + public void checkSubjectAccess(Individual subject, SubjectPartitionData subjectPartitionData) throws TxDataPartitionAccessDeniedException { SubjectPartitionCheckStatus subjectPartitionCheckStatus = accessControlService.checkSubjectAccess(subject, subjectPartitionData); if (!subjectPartitionCheckStatus.isPassed()) { - throw new BadRequestError(subjectPartitionCheckStatus.getMessage()); + throw new TxDataPartitionAccessDeniedException(subjectPartitionCheckStatus.getMessage()); } } - public void checkSubjectAccess(Individual subject) { + public void checkSubjectAccess(Individual subject) throws TxDataPartitionAccessDeniedException { this.checkSubjectAccess(subject, SubjectPartitionData.create(subject)); } + + public static class TxDataPartitionAccessDeniedException extends Exception { + public TxDataPartitionAccessDeniedException(String message) { + super(message); + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/AvniEntityResponse.java b/avni-server-api/src/main/java/org/avni/server/web/response/AvniEntityResponse.java index 554f731cb..c870e935f 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/AvniEntityResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/AvniEntityResponse.java @@ -6,12 +6,23 @@ public class AvniEntityResponse { private long id; private String uuid; + private boolean success; + private String errorMessage; + + private AvniEntityResponse() { + } + public AvniEntityResponse(CHSBaseEntity entity) { this.id = entity.getId(); this.uuid = entity.getUuid(); + this.success = true; } - public AvniEntityResponse() { + public static AvniEntityResponse error(String errorMessage) { + AvniEntityResponse response = new AvniEntityResponse(); + response.success = false; + response.errorMessage = errorMessage; + return response; } public long getId() { @@ -29,4 +40,12 @@ public String getUuid() { public void setUuid(String uuid) { this.uuid = uuid; } + + public boolean isSuccess() { + return success; + } + + public String getErrorMessage() { + return errorMessage; + } } From c0d3f8f9b334812de2c9f72ce12b6c7f6aec2b54 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 6 Aug 2024 18:07:03 +0530 Subject: [PATCH 063/129] avniproject/avni-webapp#1303 - support for Id concept data type --- .../avni/server/dao/search/BaseSubjectSearchQueryBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/search/BaseSubjectSearchQueryBuilder.java b/avni-server-api/src/main/java/org/avni/server/dao/search/BaseSubjectSearchQueryBuilder.java index b0c7c56bd..e2d36ed5f 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/search/BaseSubjectSearchQueryBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/search/BaseSubjectSearchQueryBuilder.java @@ -283,7 +283,7 @@ public T withConceptsFilter(List concept) { whereClauses.add("(" + codedFilter + ")"); } - if (c.getDataType().equalsIgnoreCase("TEXT")) { + if (c.getDataType().equalsIgnoreCase("TEXT") || c.getDataType().equalsIgnoreCase("ID")) { String value = "%" + c.getValue() + "%"; String param = "textValue" + ci; addParameter(param, value); From a04724be631d5d2f1452e4d6d80a2358510404a6 Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 7 Aug 2024 11:20:56 +0530 Subject: [PATCH 064/129] #764 | correct method name spelling --- .../server/web/contract/DashboardFilterConfigContract.java | 4 ++-- .../web/request/reports/DashboardFilterConfigRequest.java | 2 +- .../web/response/reports/DashboardFilterConfigResponse.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java index 98677bcd3..b9443301e 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java @@ -66,9 +66,9 @@ public JsonObject toJsonObject() { if (filterType.equals(DashboardFilter.FilterType.GroupSubject)) jsonObject.put(DashboardFilter.DashboardFilterConfig.GroupSubjectTypeFilterName, getGroupSubjectTypeFilter().getJsonObject()); else if (filterType.equals(DashboardFilter.FilterType.Concept)) - jsonObject.put(DashboardFilter.DashboardFilterConfig.ObservationBasedFilterName, getObsverationTypeFilterJsonObject()); + jsonObject.put(DashboardFilter.DashboardFilterConfig.ObservationBasedFilterName, getObservationTypeFilterJsonObject()); return jsonObject; } - protected abstract Object getObsverationTypeFilterJsonObject(); + protected abstract Object getObservationTypeFilterJsonObject(); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java index 49d47ef1c..68323b4e2 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java @@ -16,7 +16,7 @@ public void setObservationBasedFilter(ObservationBasedFilterRequest observationB } @Override - protected Object getObsverationTypeFilterJsonObject() { + protected Object getObservationTypeFilterJsonObject() { return observationBasedFilter.getJsonObject(); } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java b/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java index 2bb42b339..10708a847 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java @@ -16,7 +16,7 @@ public void setObservationBasedFilter(ObservationBasedFilterResponse observation } @Override - protected Object getObsverationTypeFilterJsonObject() { + protected Object getObservationTypeFilterJsonObject() { return observationBasedFilter.getJsonObject(); } } From 27e62ff2005d5b6dd623dd73931369ea3acba383 Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 7 Aug 2024 12:42:42 +0530 Subject: [PATCH 065/129] #747 | correct BadRequestError string for bySyncConcept --- .../java/org/avni/server/web/SubjectMigrationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java index 7a0395040..eec872994 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/SubjectMigrationController.java @@ -126,7 +126,7 @@ public ResponseEntity migrate(@RequestParam(value = "mode", defaultValue = "byAd } if (mode == SubjectMigrationService.BulkSubjectMigrationModes.bySyncConcept && (bulkSubjectMigrationRequest.getDestinationSyncConcepts() == null || bulkSubjectMigrationRequest.getDestinationSyncConcepts().isEmpty())) { - throw new BadRequestError("destinationSyncConcepts is required for mode: bySyncConcepts"); + throw new BadRequestError("destinationSyncConcepts is required for mode: bySyncConcept"); } UserContext userContext = UserContextHolder.getUserContext(); From 943f2047af2c9f980b36dd037680e1f3ab1df514 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 7 Aug 2024 13:43:51 +0530 Subject: [PATCH 066/129] avniproject/avni-webapp#1304 - moved category and status to database --- .../dao/OrganisationCategoryRepository.java | 16 +++ .../dao/OrganisationStatusRepository.java | 16 +++ .../org/avni/server/domain/Organisation.java | 14 ++- .../organisation/OrganisationCategory.java | 18 +++- .../organisation/OrganisationStatus.java | 17 ++- .../framework/rest/RepositoryConfig.java | 4 + .../server/web/ImplementationController.java | 2 +- .../server/web/OrganisationController.java | 14 ++- .../web/request/OrganisationContract.java | 32 +++--- .../web/response/UserInfoWebResponse.java | 10 +- .../migration/V1_339_3__OrgCategoryTable.sql | 30 ++++++ .../db/migration/V1_339_4__OrgStatusTable.sql | 25 +++++ .../db/migration/V1_339_5__AddVersions.sql | 2 + .../factory/TestOrganisationBuilder.java | 2 +- .../service/builder/TestDataSetupService.java | 10 +- .../builder/TestOrganisationSetupService.java | 10 +- .../test-data-openchs-organisation.sql | 100 ++++++++++++------ .../src/test/resources/test-data.sql | 12 +-- 18 files changed, 258 insertions(+), 76 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/dao/OrganisationCategoryRepository.java create mode 100644 avni-server-api/src/main/java/org/avni/server/dao/OrganisationStatusRepository.java create mode 100644 avni-server-api/src/main/resources/db/migration/V1_339_3__OrgCategoryTable.sql create mode 100644 avni-server-api/src/main/resources/db/migration/V1_339_4__OrgStatusTable.sql create mode 100644 avni-server-api/src/main/resources/db/migration/V1_339_5__AddVersions.sql diff --git a/avni-server-api/src/main/java/org/avni/server/dao/OrganisationCategoryRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/OrganisationCategoryRepository.java new file mode 100644 index 000000000..16b384118 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/dao/OrganisationCategoryRepository.java @@ -0,0 +1,16 @@ +package org.avni.server.dao; + +import org.avni.server.domain.organisation.OrganisationCategory; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.data.rest.core.annotation.RestResource; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RepositoryRestResource(collectionResourceRel = "organisationCategory", path = "organisationCategory") +public interface OrganisationCategoryRepository extends AvniJpaRepository, CHSRepository { + @RestResource(path = "findAllById", rel = "findAllById") + List findByIdIn(@Param("ids") Long[] ids); +} diff --git a/avni-server-api/src/main/java/org/avni/server/dao/OrganisationStatusRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/OrganisationStatusRepository.java new file mode 100644 index 000000000..6bc96a9e2 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/dao/OrganisationStatusRepository.java @@ -0,0 +1,16 @@ +package org.avni.server.dao; + +import org.avni.server.domain.organisation.OrganisationStatus; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.data.rest.core.annotation.RestResource; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RepositoryRestResource(collectionResourceRel = "organisationStatus", path = "organisationStatus") +public interface OrganisationStatusRepository extends AvniJpaRepository, CHSRepository { + @RestResource(path = "findAllById", rel = "findAllById") + List findByIdIn(@Param("ids") Long[] ids); +} diff --git a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java index 7acec03b5..aacc528b8 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/Organisation.java @@ -26,11 +26,11 @@ public class Organisation extends ETLEntity { private Account account; @NotNull - @Enumerated(EnumType.STRING) + @ManyToOne(fetch = FetchType.EAGER) private OrganisationCategory category; @NotNull - @Enumerated(EnumType.STRING) + @ManyToOne(fetch = FetchType.EAGER) private OrganisationStatus status; public Organisation() { @@ -86,14 +86,20 @@ public String getEffectiveUsernameSuffix() { return usernameSuffix == null ? getDbUser() : usernameSuffix; } + @JsonIgnore public OrganisationCategory getCategory() { return category; } + public Long getCategoryId() { + return category.getId(); + } + public void setCategory(OrganisationCategory organisationCategory) { this.category = organisationCategory; } + @JsonIgnore public OrganisationStatus getStatus() { return status; } @@ -101,4 +107,8 @@ public OrganisationStatus getStatus() { public void setStatus(OrganisationStatus status) { this.status = status; } + + public Long getStatusId() { + return status.getId(); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java index fbf5f7fab..c38282fe4 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationCategory.java @@ -1,5 +1,19 @@ package org.avni.server.domain.organisation; -public enum OrganisationCategory { - Production, UAT, Prototype, Temporary, Trial +import org.avni.server.domain.CHSEntity; + +import javax.persistence.Entity; + +@Entity +public class OrganisationCategory extends CHSEntity { + public static final String Production = "Production"; + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java index f4fabec0f..eff982d21 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/organisation/OrganisationStatus.java @@ -1,5 +1,18 @@ package org.avni.server.domain.organisation; -public enum OrganisationStatus { - Archived, Live +import org.avni.server.domain.CHSEntity; + +import javax.persistence.Entity; + +@Entity +public class OrganisationStatus extends CHSEntity { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/framework/rest/RepositoryConfig.java b/avni-server-api/src/main/java/org/avni/server/framework/rest/RepositoryConfig.java index 4e7604e55..336de246c 100644 --- a/avni-server-api/src/main/java/org/avni/server/framework/rest/RepositoryConfig.java +++ b/avni-server-api/src/main/java/org/avni/server/framework/rest/RepositoryConfig.java @@ -3,6 +3,8 @@ import org.avni.server.domain.*; import org.avni.server.domain.accessControl.GroupPrivilege; import org.avni.server.domain.accessControl.Privilege; +import org.avni.server.domain.organisation.OrganisationCategory; +import org.avni.server.domain.organisation.OrganisationStatus; import org.springframework.context.annotation.Configuration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter; @@ -37,6 +39,8 @@ public void configureRepositoryRestConfiguration(RepositoryRestConfiguration con config.exposeIdsFor(CommentThread.class); config.exposeIdsFor(RuleFailureTelemetry.class); config.exposeIdsFor(Individual.class); + config.exposeIdsFor(OrganisationCategory.class); + config.exposeIdsFor(OrganisationStatus.class); //TODO /** diff --git a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java index ba4ff1530..ac98b5080 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java @@ -109,7 +109,7 @@ public ResponseEntity delete(@Param("deleteMetadata") boolean deleteMetadata) { return new ResponseEntity<>("Super admin cannot delete implementation data", HttpStatus.FORBIDDEN); } Organisation organisation = organisationService.getCurrentOrganisation(); - if (OrganisationCategory.Production.equals(organisation.getCategory())) { + if (OrganisationCategory.Production.equals(organisation.getCategory().getName())) { return new ResponseEntity<>("Production organisation's data cannot be deleted", HttpStatus.CONFLICT); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java index 44543564a..c6fc8a43e 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java @@ -26,14 +26,18 @@ public class OrganisationController implements RestControllerResourceProcessor privileges; private boolean isAdmin; + private String organisationCategoryName; private UserInfoWebResponse() { } @@ -29,7 +29,7 @@ public static UserInfoWebResponse createForAdminUser(List getPrivileges() { @@ -62,7 +62,7 @@ public void setLastSessionTime(long lastSessionTime) { this.lastSessionTime = lastSessionTime; } - public OrganisationCategory getOrganisationCategory() { - return organisationCategory; + public String getOrganisationCategoryName() { + return organisationCategoryName; } } diff --git a/avni-server-api/src/main/resources/db/migration/V1_339_3__OrgCategoryTable.sql b/avni-server-api/src/main/resources/db/migration/V1_339_3__OrgCategoryTable.sql new file mode 100644 index 000000000..2ad596539 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_339_3__OrgCategoryTable.sql @@ -0,0 +1,30 @@ +create table if not exists organisation_category +( + id serial primary key, + uuid varchar(255) not null, + is_voided boolean NOT NULL DEFAULT FALSE, + name varchar(255) not null, + created_date_time timestamp(3) with time zone not null, + last_modified_date_time timestamp(3) with time zone not null, + created_by_id int not null, + last_modified_by_id int not null +); + +insert into organisation_category (uuid, name, created_date_time, last_modified_date_time, created_by_id, last_modified_by_id) +values ('71e1bf3b-48fb-4d4f-90f3-71c39e15fbf0', 'Production', now(), now(), 1, 1), + ('95e89458-c152-4557-9929-85f1a275d6a3', 'UAT', now(), now(), 1, 1), + ('283af4ea-0024-4440-857f-c8a82328a61d', 'Prototype', now(), now(), 1, 1), + ('f0b0a48d-8d4b-4d13-8956-c1bc577b4971', 'Temporary', now(), now(), 1, 1), + ('470ecdab-f7be-4336-a52a-1fa280080168', 'Trial', now(), now(), 1, 1), + ('d75e667e-b7ea-40dd-8d85-1328943d3b65', 'Training', now(), now(), 1, 1), + ('27eeb3e7-2396-45ac-ba50-b1b50690bcfc', 'Dev', now(), now(), 1, 1); + +alter table organisation add column if not exists category_id int null; + +update organisation set category_id = organisation_category.id +from organisation_category +where organisation.category = organisation_category.name; + +alter table organisation drop column category; + +alter table organisation alter column category_id set not null; diff --git a/avni-server-api/src/main/resources/db/migration/V1_339_4__OrgStatusTable.sql b/avni-server-api/src/main/resources/db/migration/V1_339_4__OrgStatusTable.sql new file mode 100644 index 000000000..a6696e0b6 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_339_4__OrgStatusTable.sql @@ -0,0 +1,25 @@ +create table if not exists organisation_status +( + id serial primary key, + uuid varchar(255) not null, + is_voided boolean NOT NULL DEFAULT FALSE, + name varchar(255) not null, + created_date_time timestamp(3) with time zone not null, + last_modified_date_time timestamp(3) with time zone not null, + created_by_id int not null, + last_modified_by_id int not null +); + +insert into organisation_status (uuid, name, created_date_time, last_modified_date_time, created_by_id, last_modified_by_id) +values ('338be2e2-d0e5-4186-b113-b8197ce879c5', 'Live', now(), now(), 1, 1), + ('7e609db3-ff79-472c-8f28-5b12933faaf5', 'Archived', now(), now(), 1, 1); + +alter table organisation add column if not exists status_id int null; + +update organisation set status_id = organisation_status.id +from organisation_status +where organisation.status = organisation_status.name; + +alter table organisation drop column status; + +alter table organisation alter column status_id set not null; diff --git a/avni-server-api/src/main/resources/db/migration/V1_339_5__AddVersions.sql b/avni-server-api/src/main/resources/db/migration/V1_339_5__AddVersions.sql new file mode 100644 index 000000000..d06be3898 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_339_5__AddVersions.sql @@ -0,0 +1,2 @@ +alter table organisation_category add column if not exists version int not null default 1; +alter table organisation_status add column if not exists version int not null default 1; diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java index 3071c7b5f..6729f5939 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java @@ -12,7 +12,7 @@ public class TestOrganisationBuilder { public TestOrganisationBuilder withMandatoryFields() { String placeholder = UUID.randomUUID().toString(); - return withUuid(placeholder).withDbUser("testDbUser").withName(placeholder).withSchemaName(placeholder).setCategory(OrganisationCategory.Production).withStatus(OrganisationStatus.Live); + return withUuid(placeholder).withDbUser("testDbUser").withName(placeholder).withSchemaName(placeholder); } public TestOrganisationBuilder setId(long id) { diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java index 58d556696..d7822a58e 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java @@ -31,12 +31,20 @@ public class TestDataSetupService { private TestLocationService testLocationService; @Autowired private TestCatchmentService testCatchmentService; + @Autowired + private OrganisationCategoryRepository organisationCategoryRepository; + @Autowired + private OrganisationStatusRepository organisationStatusRepository; public TestOrganisationData setupOrganisation(String orgSuffix) { Group group = new TestGroupBuilder().withMandatoryFieldsForNewEntity().build(); User user1 = new UserBuilder().withDefaultValuesForNewEntity().userName(String.format("user@%s", orgSuffix)).withAuditUser(userRepository.getDefaultSuperAdmin()).build(); User user2 = new UserBuilder().withDefaultValuesForNewEntity().userName(String.format("user2@%s", orgSuffix)).withAuditUser(userRepository.getDefaultSuperAdmin()).build(); - Organisation organisation = new TestOrganisationBuilder().withMandatoryFields().withAccount(accountRepository.getDefaultAccount()).build(); + Organisation organisation = new TestOrganisationBuilder() + .setCategory(organisationCategoryRepository.findEntity(1L)) + .withStatus(organisationStatusRepository.findEntity(1L)) + .withMandatoryFields() + .withAccount(accountRepository.getDefaultAccount()).build(); testOrganisationService.createOrganisation(organisation, user1); testOrganisationService.createUser(organisation, user2); userRepository.save(new UserBuilder(user1).withAuditUser(user1).build()); diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestOrganisationSetupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestOrganisationSetupService.java index eef88ec57..4bfc44924 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestOrganisationSetupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestOrganisationSetupService.java @@ -29,6 +29,10 @@ public class TestOrganisationSetupService { private AccountRepository accountRepository; @Autowired private UserRepository userRepository; + @Autowired + private OrganisationCategoryRepository organisationCategoryRepository; + @Autowired + private OrganisationStatusRepository organisationStatusRepository; private Group group; private User user; @@ -36,7 +40,11 @@ public class TestOrganisationSetupService { public void setupOrganisation(AbstractControllerIntegrationTest abstractControllerIntegrationTest) { group = new TestGroupBuilder().withMandatoryFieldsForNewEntity().build(); user = new UserBuilder().withDefaultValuesForNewEntity().userName("user@example").withAuditUser(userRepository.getDefaultSuperAdmin()).build(); - Organisation organisation = new TestOrganisationBuilder().withMandatoryFields().withAccount(accountRepository.getDefaultAccount()).build(); + Organisation organisation = new TestOrganisationBuilder() + .withMandatoryFields() + .setCategory(organisationCategoryRepository.findEntity(1L)) + .withStatus(organisationStatusRepository.findEntity(1L)) + .withAccount(accountRepository.getDefaultAccount()).build(); testOrganisationService.createOrganisation(organisation, user); userRepository.save(new UserBuilder(user).withAuditUser(user).build()); abstractControllerIntegrationTest.setUser(user.getUsername()); diff --git a/avni-server-api/src/test/resources/test-data-openchs-organisation.sql b/avni-server-api/src/test/resources/test-data-openchs-organisation.sql index 6bb9ab24e..07975de20 100644 --- a/avni-server-api/src/test/resources/test-data-openchs-organisation.sql +++ b/avni-server-api/src/test/resources/test-data-openchs-organisation.sql @@ -1,35 +1,67 @@ -DELETE FROM form_element; -DELETE FROM form_element_group; -DELETE FROM form_mapping; -DELETE FROM form; -DELETE FROM encounter; -DELETE FROM program_encounter; -DELETE FROM program_enrolment; -DELETE FROM individual; -DELETE FROM program; -DELETE FROM encounter_type; -DELETE FROM concept_answer; -DELETE FROM concept; -DELETE FROM individual_relationship; -DELETE FROM individual_relationship_type; -DELETE FROM individual_relation_gender_mapping; -DELETE FROM individual_relation; -DELETE FROM gender; -DELETE FROM catchment_address_mapping; -DELETE FROM address_level; -DELETE FROM catchment; -DELETE FROM account_admin; -delete from user_group; -DELETE FROM external_system_config; -DELETE FROM organisation_config; -DELETE from message_request_queue; -DELETE from message_receiver; -DELETE from message_rule; -DELETE FROM users; -DELETE FROM subject_type; -DELETE FROM groups; -DELETE FROM group_privilege; -DELETE FROM organisation; +DELETE +FROM form_element; +DELETE +FROM form_element_group; +DELETE +FROM form_mapping; +DELETE +FROM form; +DELETE +FROM encounter; +DELETE +FROM program_encounter; +DELETE +FROM program_enrolment; +DELETE +FROM individual; +DELETE +FROM program; +DELETE +FROM encounter_type; +DELETE +FROM concept_answer; +DELETE +FROM concept; +DELETE +FROM individual_relationship; +DELETE +FROM individual_relationship_type; +DELETE +FROM individual_relation_gender_mapping; +DELETE +FROM individual_relation; +DELETE +FROM gender; +DELETE +FROM catchment_address_mapping; +DELETE +FROM address_level; +DELETE +FROM catchment; +DELETE +FROM account_admin; +delete +from user_group; +DELETE +FROM external_system_config; +DELETE +FROM organisation_config; +DELETE +from message_request_queue; +DELETE +from message_receiver; +DELETE +from message_rule; +DELETE +FROM users; +DELETE +FROM subject_type; +DELETE +FROM groups; +DELETE +FROM group_privilege; +DELETE +FROM organisation; ALTER SEQUENCE form_element_id_seq RESTART WITH 1; ALTER SEQUENCE form_element_group_id_seq RESTART WITH 1; @@ -54,8 +86,8 @@ ALTER SEQUENCE individual_relation_gender_mapping_id_seq RESTART WITH 1; ALTER SEQUENCE individual_relationship_type_id_seq RESTART WITH 1; ALTER SEQUENCE individual_relationship_id_seq RESTART WITH 1; -INSERT INTO organisation (id, name, db_user, media_directory, uuid, schema_name) -VALUES (1, 'OpenCHS', 'openchs', 'openchs_impl', '3539a906-dfae-4ec3-8fbb-1b08f35c3884', 'openchs'); +INSERT INTO organisation (id, name, db_user, media_directory, uuid, schema_name, category_id, status_id) +VALUES (1, 'OpenCHS', 'openchs', 'openchs_impl', '3539a906-dfae-4ec3-8fbb-1b08f35c3884', 'openchs', 1, 1); INSERT INTO users (id, username, uuid, organisation_id, operating_individual_scope, name) VALUES (1, 'admin', '5fed2907-df3a-4867-aef5-c87f4c78a31a', 1, 'None', 'admin'); diff --git a/avni-server-api/src/test/resources/test-data.sql b/avni-server-api/src/test/resources/test-data.sql index 8c58b2fc8..f9e4b2d94 100644 --- a/avni-server-api/src/test/resources/test-data.sql +++ b/avni-server-api/src/test/resources/test-data.sql @@ -67,14 +67,14 @@ ALTER SEQUENCE message_receiver_id_seq RESTART WITH 1; ALTER SEQUENCE message_request_queue_id_seq RESTART WITH 1; ALTER SEQUENCE message_rule_id_seq RESTART WITH 1; -INSERT into organisation(id, name, db_user, uuid, media_directory, parent_organisation_id, schema_name) -values (1, 'OpenCHS', 'openchs', '3539a906-dfae-4ec3-8fbb-1b08f35c3884', 'openchs_impl', null, 'openchs') +INSERT into organisation(id, name, db_user, uuid, media_directory, parent_organisation_id, schema_name, category_id, status_id) +values (1, 'OpenCHS', 'openchs', '3539a906-dfae-4ec3-8fbb-1b08f35c3884', 'openchs_impl', null, 'openchs', 1, 1) ON CONFLICT (uuid) DO NOTHING; select create_db_user('demo', 'password'); -INSERT INTO organisation(id, name, db_user, media_directory, uuid, parent_organisation_id, schema_name) -VALUES (2, 'demo', 'demo', 'demo', 'ae0e4ac4-681d-45f2-8bdd-2b09a5a1a6e5', 1, 'demo') +INSERT INTO organisation(id, name, db_user, media_directory, uuid, parent_organisation_id, schema_name, category_id, status_id) +VALUES (2, 'demo', 'demo', 'demo', 'ae0e4ac4-681d-45f2-8bdd-2b09a5a1a6e5', 1, 'demo', 1, 1) ON CONFLICT (uuid) DO NOTHING; insert into organisation_config (uuid, organisation_id, settings, version, is_voided, worklist_updation_rule, created_by_id, last_modified_by_id, created_date_time, last_modified_date_time) @@ -82,8 +82,8 @@ values ('5bd9c67e-c949-4872-9763-daeab7b48b1b', 1, '{"enableMessaging": true}', select create_db_user('a_demo', 'password'); -INSERT INTO organisation (id, name, db_user, media_directory, uuid, parent_organisation_id, schema_name) -VALUES (3, 'a-demo', 'a_demo', 'a-demo', '2734f2ba-610b-49f8-b8d3-4196a460e325', 1, 'a_demo') +INSERT INTO organisation (id, name, db_user, media_directory, uuid, parent_organisation_id, schema_name, category_id, status_id) +VALUES (3, 'a-demo', 'a_demo', 'a-demo', '2734f2ba-610b-49f8-b8d3-4196a460e325', 1, 'a_demo', 1, 1) ON CONFLICT (uuid) DO NOTHING; insert into subject_type(id, uuid, name, organisation_id, created_by_id, last_modified_by_id, created_date_time, last_modified_date_time) From 2b44ce0563f64dadae18ebb24891098c4fd5d724 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 8 Aug 2024 13:41:27 +0530 Subject: [PATCH 067/129] avniproject/avni-webapp#1304 - function to grant permission only on table --- .../resources/db/migration/R__Functions.sql | 21 +++++++++++++++++ ...1_339_6__GrantPermissionOnNewOrgTables.sql | 23 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 avni-server-api/src/main/resources/db/migration/V1_339_6__GrantPermissionOnNewOrgTables.sql diff --git a/avni-server-api/src/main/resources/db/migration/R__Functions.sql b/avni-server-api/src/main/resources/db/migration/R__Functions.sql index d7c2a3662..5c9dc3023 100644 --- a/avni-server-api/src/main/resources/db/migration/R__Functions.sql +++ b/avni-server-api/src/main/resources/db/migration/R__Functions.sql @@ -201,6 +201,27 @@ BEGIN END; $$; +CREATE OR REPLACE FUNCTION grant_all_on_table(rolename text, tablename text) + RETURNS text AS +$body$ +BEGIN + EXECUTE ( + SELECT 'GRANT ALL ON TABLE ' + || tablename + || ' TO ' || quote_ident(rolename) + ); + + EXECUTE ( + SELECT 'GRANT SELECT ON ' + || tablename + || ' TO ' || quote_ident(rolename) + ); + + EXECUTE 'GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ' || quote_ident(rolename) || ''; + RETURN 'ALL PERMISSIONS GRANTED TO ' || quote_ident(rolename); +END; +$body$ LANGUAGE plpgsql; + CREATE OR REPLACE FUNCTION grant_all_on_all(rolename text) RETURNS text AS $body$ diff --git a/avni-server-api/src/main/resources/db/migration/V1_339_6__GrantPermissionOnNewOrgTables.sql b/avni-server-api/src/main/resources/db/migration/V1_339_6__GrantPermissionOnNewOrgTables.sql new file mode 100644 index 000000000..72e1dd5b2 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_339_6__GrantPermissionOnNewOrgTables.sql @@ -0,0 +1,23 @@ +CREATE OR REPLACE FUNCTION grant_all_on_table(rolename text, tablename text) + RETURNS text AS +$body$ +BEGIN + EXECUTE ( + SELECT 'GRANT ALL ON TABLE ' + || tablename + || ' TO ' || quote_ident(rolename) + ); + + EXECUTE ( + SELECT 'GRANT SELECT ON ' + || tablename + || ' TO ' || quote_ident(rolename) + ); + + EXECUTE 'GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ' || quote_ident(rolename) || ''; + RETURN 'ALL PERMISSIONS GRANTED TO ' || quote_ident(rolename); +END; +$body$ LANGUAGE plpgsql; + +SELECT grant_all_on_table(a.rolname, 'organisation_category') FROM pg_roles a WHERE pg_has_role('openchs', a.oid, 'member'); +SELECT grant_all_on_table(a.rolname, 'organisation_status') FROM pg_roles a WHERE pg_has_role('openchs', a.oid, 'member'); From e9f4e219b4108ab4967556e35162f40c33cf06c6 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 8 Aug 2024 13:41:27 +0530 Subject: [PATCH 068/129] avniproject/avni-webapp#1304 - function to grant permission only on table --- .../resources/db/migration/R__Functions.sql | 21 +++++++++++++++++ ...1_339_6__GrantPermissionOnNewOrgTables.sql | 23 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 avni-server-api/src/main/resources/db/migration/V1_339_6__GrantPermissionOnNewOrgTables.sql diff --git a/avni-server-api/src/main/resources/db/migration/R__Functions.sql b/avni-server-api/src/main/resources/db/migration/R__Functions.sql index d7c2a3662..5c9dc3023 100644 --- a/avni-server-api/src/main/resources/db/migration/R__Functions.sql +++ b/avni-server-api/src/main/resources/db/migration/R__Functions.sql @@ -201,6 +201,27 @@ BEGIN END; $$; +CREATE OR REPLACE FUNCTION grant_all_on_table(rolename text, tablename text) + RETURNS text AS +$body$ +BEGIN + EXECUTE ( + SELECT 'GRANT ALL ON TABLE ' + || tablename + || ' TO ' || quote_ident(rolename) + ); + + EXECUTE ( + SELECT 'GRANT SELECT ON ' + || tablename + || ' TO ' || quote_ident(rolename) + ); + + EXECUTE 'GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ' || quote_ident(rolename) || ''; + RETURN 'ALL PERMISSIONS GRANTED TO ' || quote_ident(rolename); +END; +$body$ LANGUAGE plpgsql; + CREATE OR REPLACE FUNCTION grant_all_on_all(rolename text) RETURNS text AS $body$ diff --git a/avni-server-api/src/main/resources/db/migration/V1_339_6__GrantPermissionOnNewOrgTables.sql b/avni-server-api/src/main/resources/db/migration/V1_339_6__GrantPermissionOnNewOrgTables.sql new file mode 100644 index 000000000..72e1dd5b2 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_339_6__GrantPermissionOnNewOrgTables.sql @@ -0,0 +1,23 @@ +CREATE OR REPLACE FUNCTION grant_all_on_table(rolename text, tablename text) + RETURNS text AS +$body$ +BEGIN + EXECUTE ( + SELECT 'GRANT ALL ON TABLE ' + || tablename + || ' TO ' || quote_ident(rolename) + ); + + EXECUTE ( + SELECT 'GRANT SELECT ON ' + || tablename + || ' TO ' || quote_ident(rolename) + ); + + EXECUTE 'GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ' || quote_ident(rolename) || ''; + RETURN 'ALL PERMISSIONS GRANTED TO ' || quote_ident(rolename); +END; +$body$ LANGUAGE plpgsql; + +SELECT grant_all_on_table(a.rolname, 'organisation_category') FROM pg_roles a WHERE pg_has_role('openchs', a.oid, 'member'); +SELECT grant_all_on_table(a.rolname, 'organisation_status') FROM pg_roles a WHERE pg_has_role('openchs', a.oid, 'member'); From a98556f88bfa9476ca2bbd188abe307ec8fa3c56 Mon Sep 17 00:00:00 2001 From: himeshr Date: Thu, 8 Aug 2024 15:55:41 +0530 Subject: [PATCH 069/129] Revert "#768 | Add null check in get and rename var in void of identifierUserAssignment" This reverts commit 914619585460cd2a0836e07c85b7cf4a5db6adb7. --- .../java/org/avni/server/builder/ChecklistDetailBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java index 1e90d6a9d..09d6b38a8 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java @@ -49,7 +49,6 @@ public ChecklistDetailBuilder withItems(List items) .withVoided(item.isVoided()) .withform(form) .withConcept(concept) - .withLeadItem(builtItems.get(item.getDependentOn())) .withScheduleOnExpiryOfDependency(item.getScheduleOnExpiryOfDependency()) .withMinDaysFromStartDate(item.getMinDaysFromStartDate()) .withMinDaysFromDependent(item.getMinDaysFromDependent()) @@ -57,6 +56,8 @@ public ChecklistDetailBuilder withItems(List items) .build(); builtItems.put(builtItemDetail.getUuid(), builtItemDetail); }); + //set dependentOn after all items in request have been processed so order of items in request does not matter for dependents + items.forEach(item -> getExistingChecklistItemDetail(this.get(), item).setLeadChecklistItemDetail(builtItems.get(item.getDependentOn()))); return this; } From 458e8bdf8ec3f97c870945cf47cb18d8550b13ce Mon Sep 17 00:00:00 2001 From: himeshr Date: Thu, 8 Aug 2024 15:55:41 +0530 Subject: [PATCH 070/129] Revert "#768 | Add null check in get and rename var in void of identifierUserAssignment" This reverts commit 914619585460cd2a0836e07c85b7cf4a5db6adb7. --- .../java/org/avni/server/builder/ChecklistDetailBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java index 1e90d6a9d..09d6b38a8 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/ChecklistDetailBuilder.java @@ -49,7 +49,6 @@ public ChecklistDetailBuilder withItems(List items) .withVoided(item.isVoided()) .withform(form) .withConcept(concept) - .withLeadItem(builtItems.get(item.getDependentOn())) .withScheduleOnExpiryOfDependency(item.getScheduleOnExpiryOfDependency()) .withMinDaysFromStartDate(item.getMinDaysFromStartDate()) .withMinDaysFromDependent(item.getMinDaysFromDependent()) @@ -57,6 +56,8 @@ public ChecklistDetailBuilder withItems(List items) .build(); builtItems.put(builtItemDetail.getUuid(), builtItemDetail); }); + //set dependentOn after all items in request have been processed so order of items in request does not matter for dependents + items.forEach(item -> getExistingChecklistItemDetail(this.get(), item).setLeadChecklistItemDetail(builtItems.get(item.getDependentOn()))); return this; } From b4ed8fd46d4c6004fbbcd85831124e4931bce6a9 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 8 Aug 2024 16:35:59 +0530 Subject: [PATCH 071/129] #0 - minor refactoring --- .../messaging/api/MessageRuleController.java | 2 +- .../avni/server/application/FormElement.java | 3 ++- .../search/BaseSubjectSearchQueryBuilder.java | 15 +++++++------- .../batch/csv/ErrorFileCreatorListener.java | 4 ++-- .../csv/writer/UserAndCatchmentWriter.java | 5 +++-- .../server/service/SubjectTypeService.java | 20 +------------------ 6 files changed, 17 insertions(+), 32 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/messaging/api/MessageRuleController.java b/avni-server-api/src/main/java/org/avni/messaging/api/MessageRuleController.java index 706d76484..482a23332 100644 --- a/avni-server-api/src/main/java/org/avni/messaging/api/MessageRuleController.java +++ b/avni-server-api/src/main/java/org/avni/messaging/api/MessageRuleController.java @@ -71,6 +71,6 @@ public ResponseEntity findOne(@PathVariable("id") Long id ) } private boolean isAString(String s) { - return s != null && !s.isEmpty(); + return !StringUtils.isEmpty(s); } } diff --git a/avni-server-api/src/main/java/org/avni/server/application/FormElement.java b/avni-server-api/src/main/java/org/avni/server/application/FormElement.java index 4e1779903..f085cb4a5 100644 --- a/avni-server-api/src/main/java/org/avni/server/application/FormElement.java +++ b/avni-server-api/src/main/java/org/avni/server/application/FormElement.java @@ -7,6 +7,7 @@ import org.hibernate.annotations.Type; import org.joda.time.DateTime; import org.avni.server.web.validation.ValidationException; +import org.springframework.util.StringUtils; import javax.persistence.*; import javax.validation.constraints.NotNull; @@ -182,7 +183,7 @@ public boolean isVoided() { public List validate() { ArrayList validationResults = new ArrayList<>(); - if (this.getType() != null && !this.getType().trim().isEmpty()) { + if (StringUtils.isEmpty(this.getType())) { try { FormElementType.valueOf(this.getType()); } catch (IllegalArgumentException | NullPointerException e) { diff --git a/avni-server-api/src/main/java/org/avni/server/dao/search/BaseSubjectSearchQueryBuilder.java b/avni-server-api/src/main/java/org/avni/server/dao/search/BaseSubjectSearchQueryBuilder.java index 14603211c..cb9b88f86 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/search/BaseSubjectSearchQueryBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/search/BaseSubjectSearchQueryBuilder.java @@ -15,6 +15,7 @@ import org.avni.server.web.request.webapp.search.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; import java.util.*; import java.util.stream.Collectors; @@ -98,7 +99,7 @@ protected T withPaginationFilters(PageDetails pageElement) { parameters.put("limit", limit); String sortColumn = pageElement.getSortColumn(); - if (sortColumn != null && !sortColumn.isEmpty()) { + if (!StringUtils.isEmpty(sortColumn)) { SortOrder sortOrder = Optional.ofNullable(pageElement.getSortOrder()).orElse(SortOrder.asc); Map columnsMap = new HashMap() { @@ -157,7 +158,7 @@ private List getSearchFields(String subjectTypeUUID, List addressIds) { } public T withSearchAll(String searchString) { - if (searchString == null || searchString.isEmpty()) return (T) this; + if (StringUtils.isEmpty(searchString)) return (T) this; String searchValue = "%" + searchString + "%"; parameters.put("searchAll", searchValue); whereClauses.add("(cast(i.observations as text) ilike :searchAll\n" + @@ -291,13 +292,13 @@ public T withConceptsFilter(List concept) { if (c.getDataType().equalsIgnoreCase("NUMERIC")) { if (c.getWidget() != null && c.getWidget().equalsIgnoreCase("RANGE")) { - if (c.getMinValue() != null && !c.getMinValue().isEmpty()) { + if (!StringUtils.isEmpty(c.getMinValue())) { Float value = Float.parseFloat(c.getMinValue()); String param = "numericValueMin" + ci; addParameter(param, value); whereClauses.add("cast(" + tableAlias + ".observations ->> :" + conceptUuidParam + " as numeric) >= :" + param); } - if (c.getMaxValue() != null && !c.getMaxValue().isEmpty()) { + if (!StringUtils.isEmpty(c.getMaxValue())) { Float value = Float.parseFloat(c.getMaxValue()); String param = "numericValueMax" + ci; addParameter(param, value); @@ -313,13 +314,13 @@ public T withConceptsFilter(List concept) { if (c.getDataType().equalsIgnoreCase("DATE")) { if (c.getWidget() != null && c.getWidget().equalsIgnoreCase("RANGE")) { - if (c.getMinValue() != null && !c.getMinValue().isEmpty()) { + if (!StringUtils.isEmpty(c.getMinValue())) { String value = c.getMinValue(); String param = "dateTimeValueMin" + ci; addParameter(param, value); whereClauses.add("cast(" + tableAlias + ".observations ->>:" + conceptUuidParam + " as date) >= cast(:" + param + " as date)"); } - if (c.getMaxValue() != null && !c.getMaxValue().isEmpty()) { + if (!StringUtils.isEmpty(c.getMaxValue())) { String value = c.getMaxValue(); String param = "dateTimeValueMax" + ci; addParameter(param, value); diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/ErrorFileCreatorListener.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/ErrorFileCreatorListener.java index 613633314..e25bb1866 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/ErrorFileCreatorListener.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/ErrorFileCreatorListener.java @@ -77,10 +77,10 @@ public void beforeJob(JobExecution jobExecution) { @Override public void afterJob(JobExecution jobExecution) { - logger.info(format("Bulk upload '%s'! %s", jobExecution.getStatus(), jobInfo)); + logger.debug(format("Bulk upload '%s'! %s", jobExecution.getStatus(), jobInfo)); try { ObjectInfo metadata = bulkUploadS3Service.uploadErrorFile(errorFile, uuid); - logger.info(format("Bulk upload '%s'! Check for errors at '%s'", jobExecution.getStatus(), metadata.getKey())); + logger.debug(format("Bulk upload '%s'! Check for errors at '%s'", jobExecution.getStatus(), metadata.getKey())); } catch (IOException e) { logger.error(String.format("Unable to create error files in S3 %s", e.getMessage()), e); } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index 6a2afb778..155f76200 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -13,6 +13,7 @@ import org.springframework.batch.item.ItemWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import java.io.Serializable; import java.util.*; @@ -34,7 +35,7 @@ public class UserAndCatchmentWriter implements ItemWriter, Serializable { private final ConceptService conceptService; private final Pattern compoundHeaderPattern; private final ResetSyncService resetSyncService; - private final String METADATA_ROW_START_STRING = "Mandatory field."; + private static final String METADATA_ROW_START_STRING = "Mandatory field."; @Autowired public UserAndCatchmentWriter(CatchmentService catchmentService, @@ -142,7 +143,7 @@ private void updateSyncSettingsFor(String saHeader, Row row, Map constructSyncAttributeHeadersForSubjectType(SubjectType sub } public List constructSyncAttributeAllowedValuesForSubjectTypes() { - List subjectTypes = subjectTypeRepository.findByIsVoidedFalse(); - Predicate subjectTypeHasSyncAttributes = subjectType -> - Objects.nonNull(subjectType.getSyncRegistrationConcept1()) || - Objects.nonNull(subjectType.getSyncRegistrationConcept2()); - return subjectTypes.stream().sorted((a,b) -> (int) (a.getId() - b.getId())). - filter(subjectTypeHasSyncAttributes). - map(this::constructSyncAttributeAllowedValuesForSubjectType). - flatMap(Collection::stream). - collect(Collectors.toList()); - } - - private List constructSyncAttributeAllowedValuesForSubjectType(SubjectType subjectTypeWithSyncAttribute) { - String[] syncAttributes = new String[]{subjectTypeWithSyncAttribute.getSyncRegistrationConcept1(), - subjectTypeWithSyncAttribute.getSyncRegistrationConcept2()}; - - return Arrays.stream(syncAttributes). - filter(Objects::nonNull).sorted(). - map(sa -> String.format("\"Allowed values: %s\"", conceptService.getSampleValuesForSyncConcept(conceptService.get(sa)))) - .collect(Collectors.toList()); + return this.constructSyncAttributeHeadersForSubjectTypes(); } public JsonObject getDefaultSettings() { From c3236505de32cadc2da884ae176688714138dc16 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 8 Aug 2024 16:46:25 +0530 Subject: [PATCH 072/129] #0 - string check fix --- .../src/main/java/org/avni/server/application/FormElement.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/application/FormElement.java b/avni-server-api/src/main/java/org/avni/server/application/FormElement.java index f085cb4a5..351b91f0e 100644 --- a/avni-server-api/src/main/java/org/avni/server/application/FormElement.java +++ b/avni-server-api/src/main/java/org/avni/server/application/FormElement.java @@ -183,7 +183,7 @@ public boolean isVoided() { public List validate() { ArrayList validationResults = new ArrayList<>(); - if (StringUtils.isEmpty(this.getType())) { + if (!StringUtils.isEmpty(this.getType())) { try { FormElementType.valueOf(this.getType()); } catch (IllegalArgumentException | NullPointerException e) { From 72611f9b954c2c520cea6dad7dc7da0da89d26a2 Mon Sep 17 00:00:00 2001 From: Joy A Date: Mon, 12 Aug 2024 10:27:01 +0530 Subject: [PATCH 073/129] #749 | Create primary dashboard for everyone group on org creation --- .../dao/StandardReportCardTypeRepository.java | 2 + .../org/avni/server/service/CardService.java | 44 ++++++++-- .../avni/server/service/DashboardService.java | 85 ++++++++++++++++++- .../server/service/GroupDashboardService.java | 16 +++- .../server/service/OrganisationService.java | 14 ++- 5 files changed, 144 insertions(+), 17 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/StandardReportCardTypeRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/StandardReportCardTypeRepository.java index 979f9297e..ac003030e 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/StandardReportCardTypeRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/StandardReportCardTypeRepository.java @@ -26,4 +26,6 @@ Page findByLastModifiedDateTimeIsBetweenOrderByLastModif Pageable pageable); boolean existsByLastModifiedDateTimeGreaterThan(DateTime lastModifiedDateTime); + + List findAllByNameIn(List defaultDashboardStandardCardTypeNames); } diff --git a/avni-server-api/src/main/java/org/avni/server/service/CardService.java b/avni-server-api/src/main/java/org/avni/server/service/CardService.java index ada409565..ecd92efb5 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CardService.java @@ -14,7 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.List; +import java.util.*; @Service public class CardService implements NonScopeAwareService { @@ -109,12 +109,16 @@ private void buildStandardReportCardInputs(StandardReportCardType type, List createDefaultDashboardCards(Organisation organisation) { + List defaultDashboardStandardCardTypeNames = Arrays.asList("Scheduled visits", "Overdue visits", "Total", "Recent registrations", "Recent enrolments", "Recent visits", "Due checklist"); + List standardReportCardTypes = standardReportCardTypeRepository.findAllByNameIn(defaultDashboardStandardCardTypeNames); + Map savedCards = new HashMap<>(); + standardReportCardTypes.forEach(standardReportCardType -> { + ReportCard reportCard = new ReportCard(); + reportCard.setUuid(UUID.randomUUID().toString()); + reportCard.setStandardReportCardType(standardReportCardType); + reportCard.setOrganisationId(organisation.getId()); + reportCard.setName(standardReportCardType.getName()); + reportCard.setColour("#ffffff"); + reportCard.setStandardReportCardInputPrograms(Collections.emptyList()); + reportCard.setStandardReportCardInputEncounterTypes(Collections.emptyList()); + reportCard.setStandardReportCardInputSubjectTypes(Collections.emptyList()); + if (isRecentStandardReportCard(standardReportCardType.getName())) { + reportCard.setStandardReportCardInputRecentDuration(new ValueUnit("1", "days")); + } + if (standardReportCardType.getName().equals("Overdue visits")) { + reportCard.setColour("#d32f2f"); + } + if (standardReportCardType.getName().equals("Scheduled visits")) { + reportCard.setColour("#388e3c"); + } + savedCards.put(reportCard.getName(), cardRepository.save(reportCard)); + }); + return savedCards; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java index f7a075ca9..80d633c0c 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java @@ -9,13 +9,15 @@ import org.avni.server.web.contract.reports.DashboardBundleContract; import org.avni.server.web.contract.reports.DashboardSectionBundleContract; import org.avni.server.web.contract.reports.DashboardSectionCardMappingBundleContract; -import org.avni.server.web.request.*; +import org.avni.server.web.request.DashboardFilterRequest; +import org.avni.server.web.request.DashboardFilterResponse; +import org.avni.server.web.request.DashboardWebRequest; import org.avni.server.web.request.reports.DashboardSectionCardMappingRequest; import org.avni.server.web.request.reports.DashboardSectionWebRequest; import org.avni.server.web.response.reports.DashboardFilterConfigResponse; +import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.joda.time.DateTime; import java.util.*; @@ -26,16 +28,18 @@ public class DashboardService implements NonScopeAwareService { private final DashboardSectionRepository dashboardSectionRepository; private final DashboardSectionCardMappingRepository dashboardSectionCardMappingRepository; private final DashboardFilterRepository dashboardFilterRepository; + private final CardService cardService; @Autowired public DashboardService(DashboardRepository dashboardRepository, CardRepository cardRepository, - DashboardSectionRepository dashboardSectionRepository, DashboardSectionCardMappingRepository dashboardSectionCardMappingRepository, DashboardFilterRepository dashboardFilterRepository) { + DashboardSectionRepository dashboardSectionRepository, DashboardSectionCardMappingRepository dashboardSectionCardMappingRepository, DashboardFilterRepository dashboardFilterRepository, CardService cardService) { this.dashboardRepository = dashboardRepository; this.cardRepository = cardRepository; this.dashboardSectionRepository = dashboardSectionRepository; this.dashboardSectionCardMappingRepository = dashboardSectionCardMappingRepository; this.dashboardFilterRepository = dashboardFilterRepository; + this.cardService = cardService; } public Dashboard saveDashboard(DashboardWebRequest dashboardRequest) { @@ -209,4 +213,79 @@ public void saveDashboards(DashboardBundleContract[] dashboardContracts) { } } } + + public Dashboard createDefaultDashboard(Organisation organisation) { + Map defaultDashboardCards = cardService.createDefaultDashboardCards(organisation); + Dashboard defaultDashboard = createDashboard(organisation, "Default Dashboard"); + + DashboardSection visitDetailsSection = createDashboardSection(organisation, "Visit Details", 1.0); + DashboardSectionCardMapping scheduledVisitsToVisitDetailsMapping = createDashboardSectionCardMapping(organisation, defaultDashboardCards.get("Scheduled visits"), visitDetailsSection, 1.0); + visitDetailsSection.addDashboardSectionCardMapping(scheduledVisitsToVisitDetailsMapping); + DashboardSectionCardMapping overdueVisitsToVisitDetailsMapping = createDashboardSectionCardMapping(organisation, defaultDashboardCards.get("Overdue visits"), visitDetailsSection, 2.0); + visitDetailsSection.addDashboardSectionCardMapping(overdueVisitsToVisitDetailsMapping); + defaultDashboard.addSection(visitDetailsSection); + + DashboardSection recentStatisticsSection = createDashboardSection(organisation, "Recent Statistics", 2.0); + DashboardSectionCardMapping recentRegistrationsToStatisticsMapping = createDashboardSectionCardMapping(organisation, defaultDashboardCards.get("Recent registrations"), recentStatisticsSection, 1.0); + recentStatisticsSection.addDashboardSectionCardMapping(recentRegistrationsToStatisticsMapping); + DashboardSectionCardMapping recentEnrolmentsToStatisticsMapping = createDashboardSectionCardMapping(organisation, defaultDashboardCards.get("Recent enrolments"), recentStatisticsSection, 2.0); + recentStatisticsSection.addDashboardSectionCardMapping(recentEnrolmentsToStatisticsMapping); + DashboardSectionCardMapping recentVisitsToStatisticsMapping = createDashboardSectionCardMapping(organisation, defaultDashboardCards.get("Recent visits"), recentStatisticsSection, 3.0); + recentStatisticsSection.addDashboardSectionCardMapping(recentVisitsToStatisticsMapping); + defaultDashboard.addSection(recentStatisticsSection); + + DashboardSection registrationOverviewSection = createDashboardSection(organisation, "Registration Overview", 3.0); + DashboardSectionCardMapping totalToRegistrationOverviewMapping = createDashboardSectionCardMapping(organisation, defaultDashboardCards.get("Total"), registrationOverviewSection, 1.0); + registrationOverviewSection.addDashboardSectionCardMapping(totalToRegistrationOverviewMapping); + defaultDashboard.addSection(registrationOverviewSection); + + DashboardFilter subjectTypeFilter = createDashboardFilter(organisation, "Subject Type", new JsonObject() + .with(DashboardFilter.DashboardFilterConfig.TypeFieldName, String.valueOf(DashboardFilter.FilterType.SubjectType))); + defaultDashboard.addUpdateFilter(subjectTypeFilter); + + DashboardFilter asOnDateFilter = createDashboardFilter(organisation, "As On Date", new JsonObject() + .with(DashboardFilter.DashboardFilterConfig.TypeFieldName, String.valueOf(DashboardFilter.FilterType.AsOnDate))); + defaultDashboard.addUpdateFilter(asOnDateFilter); + + return dashboardRepository.save(defaultDashboard); + } + + private OrganisationAwareEntity setDefaults(OrganisationAwareEntity entity, Organisation organisation) { + entity.assignUUID(); + entity.setOrganisationId(organisation.getId()); + return entity; + } + + private Dashboard createDashboard(Organisation organisation, String name) { + Dashboard dashboard = new Dashboard(); + setDefaults(dashboard, organisation); + dashboard.setName(name); + return dashboard; + } + + private DashboardSection createDashboardSection(Organisation organisation, String name, Double displayOrder) { + DashboardSection dashboardSection = new DashboardSection(); + setDefaults(dashboardSection, organisation); + dashboardSection.setName(name); + dashboardSection.setViewType(DashboardSection.ViewType.Default); + dashboardSection.setDisplayOrder(displayOrder); + return dashboardSection; + } + + private DashboardSectionCardMapping createDashboardSectionCardMapping(Organisation organisation, ReportCard reportCard, DashboardSection dashboardSection, Double displayOrder) { + DashboardSectionCardMapping dashboardSectionCardMapping = new DashboardSectionCardMapping(); + setDefaults(dashboardSectionCardMapping, organisation); + dashboardSectionCardMapping.setCard(reportCard); + dashboardSectionCardMapping.setDashboardSection(dashboardSection); + dashboardSectionCardMapping.setDisplayOrder(displayOrder); + return dashboardSectionCardMapping; + } + + private DashboardFilter createDashboardFilter(Organisation organisation, String name, JsonObject filterConfig) { + DashboardFilter dashboardFilter = new DashboardFilter(); + setDefaults(dashboardFilter, organisation); + dashboardFilter.setName(name); + dashboardFilter.setFilterConfig(filterConfig); + return dashboardFilter; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java index b699cd2f0..1636cb710 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/GroupDashboardService.java @@ -4,10 +4,7 @@ import org.avni.server.dao.DashboardRepository; import org.avni.server.dao.GroupDashboardRepository; import org.avni.server.dao.GroupRepository; -import org.avni.server.domain.Dashboard; -import org.avni.server.domain.Group; -import org.avni.server.domain.GroupDashboard; -import org.avni.server.domain.ValidationException; +import org.avni.server.domain.*; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.web.contract.GroupDashboardBundleContract; import org.avni.server.web.request.GroupDashboardContract; @@ -18,6 +15,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; @Service public class GroupDashboardService implements NonScopeAwareService { @@ -107,6 +105,16 @@ public void delete(GroupDashboard groupDashboard) { groupDashboardRepository.save(groupDashboard); } + public void createDefaultGroupDashboardForOrg(Organisation organisation, Group group, Dashboard dashboard) { + GroupDashboard groupDashboard = new GroupDashboard(); + groupDashboard.setOrganisationId(organisation.getId()); + groupDashboard.setUuid(UUID.randomUUID().toString()); + groupDashboard.setGroup(group); + groupDashboard.setDashboard(dashboard); + groupDashboard.setPrimaryDashboard(true); + groupDashboardRepository.save(groupDashboard); + } + @Override public boolean isNonScopeEntityChanged(DateTime lastModifiedDateTime) { return groupDashboardRepository.existsByLastModifiedDateTimeGreaterThan(lastModifiedDateTime); diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index 3b55c341a..36faf008d 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -107,6 +107,7 @@ public class OrganisationService { private final DashboardSectionCardMappingRepository dashboardSectionCardMappingRepository; private final DashboardSectionRepository dashboardSectionRepository; private final GroupDashboardRepository groupDashboardRepository; + private final DashboardService dashboardService; private final Msg91ConfigRepository msg91ConfigRepository; private final S3Service s3Service; @@ -146,6 +147,7 @@ public class OrganisationService { private final UserSubjectRepository userSubjectRepository; private final Logger logger; private final DashboardMapper dashboardMapper; + private final GroupDashboardService groupDashboardService; @Autowired public OrganisationService(FormRepository formRepository, @@ -225,7 +227,7 @@ public OrganisationService(FormRepository formRepository, ReportCardMapper reportCardMapper, DashboardMapper dashboardMapper, UserSubjectRepository userSubjectRepository, - DashboardFilterRepository dashboardFilterRepository) { + DashboardFilterRepository dashboardFilterRepository, GroupDashboardService groupDashboardService) { this.formRepository = formRepository; this.addressLevelTypeRepository = addressLevelTypeRepository; this.locationRepository = locationRepository; @@ -304,7 +306,9 @@ public OrganisationService(FormRepository formRepository, this.reportCardMapper = reportCardMapper; this.dashboardMapper = dashboardMapper; this.userSubjectRepository = userSubjectRepository; + this.dashboardService = dashboardService; logger = LoggerFactory.getLogger(this.getClass()); + this.groupDashboardService = groupDashboardService; } public void addOrganisationConfig(Long orgId, ZipOutputStream zos) throws IOException { @@ -774,14 +778,14 @@ private void createGender(String genderName, Organisation org) { genderRepository.save(gender); } - private void addDefaultGroup(Long organisationId, String groupType) { + private Group addDefaultGroup(Long organisationId, String groupType) { Group group = new Group(); group.setName(groupType); group.setOrganisationId(organisationId); group.setUuid(UUID.randomUUID().toString()); group.setHasAllPrivileges(group.isAdministrator()); group.setVersion(0); - groupRepository.save(group); + return groupRepository.save(group); } private void createDefaultGenders(Organisation org) { @@ -792,9 +796,11 @@ private void createDefaultGenders(Organisation org) { public void setupBaseOrganisationData(Organisation organisation) { createDefaultGenders(organisation); - addDefaultGroup(organisation.getId(), Group.Everyone); + Group everyoneGroup = addDefaultGroup(organisation.getId(), Group.Everyone); addDefaultGroup(organisation.getId(), Group.Administrators); organisationConfigService.createDefaultOrganisationConfig(organisation); + Dashboard defaultDashboard = dashboardService.createDefaultDashboard(organisation); + groupDashboardService.createDefaultGroupDashboardForOrg(organisation, everyoneGroup, defaultDashboard); } public Organisation getCurrentOrganisation() { From 61c91e55fb8d01eee1bf0d48cc222d1e0be86e80 Mon Sep 17 00:00:00 2001 From: himeshr Date: Mon, 12 Aug 2024 10:57:30 +0530 Subject: [PATCH 074/129] avniproject/avni-webapp#787 | WIP: Delete all org data refactoring draft --- .../org/avni/server/dao/UserRepository.java | 1 + .../server/service/OrganisationService.java | 186 ++++++++++++++++-- .../avni/server/service/StorageService.java | 1 + .../org/avni/server/service/UserService.java | 22 ++- .../server/web/ImplementationController.java | 49 ++++- .../org/avni/server/web/UserController.java | 12 +- .../UserServiceAssociateGroupsTest.java | 5 +- .../avni/server/service/UserServiceTest.java | 4 +- 8 files changed, 239 insertions(+), 41 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java index d232a0a7b..bd635817c 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/UserRepository.java @@ -51,6 +51,7 @@ Page findByOrganisationIdAndIsVoidedFalse(@Param("organisationId") Long or List findAllByOrganisationIdAndIsVoidedFalse(Long organisationId); List findAllByIsVoidedFalseAndOrganisationId(Long organisationId); + List findAllByOrganisationId(Long organisationId); @Query(value = "SELECT u FROM User u left join u.accountAdmin as aa " + "where u.isVoided = false " + diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index 9bdc10c6d..1319dbf52 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -4,22 +4,22 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import org.apache.commons.io.IOUtils; import org.avni.messaging.contract.MessageRuleContract; +import org.avni.messaging.repository.ManualMessageRepository; import org.avni.messaging.repository.MessageReceiverRepository; import org.avni.messaging.repository.MessageRequestQueueRepository; +import org.avni.messaging.repository.MessageRuleRepository; import org.avni.messaging.service.MessagingService; import org.avni.server.application.Form; import org.avni.server.application.FormMapping; import org.avni.server.dao.*; -import org.avni.server.dao.application.FormElementGroupRepository; -import org.avni.server.dao.application.FormElementRepository; -import org.avni.server.dao.application.FormMappingRepository; -import org.avni.server.dao.application.FormRepository; -import org.avni.server.dao.individualRelationship.IndividualRelationGenderMappingRepository; -import org.avni.server.dao.individualRelationship.IndividualRelationRepository; -import org.avni.server.dao.individualRelationship.IndividualRelationshipRepository; -import org.avni.server.dao.individualRelationship.IndividualRelationshipTypeRepository; +import org.avni.server.dao.application.*; +import org.avni.server.dao.externalSystem.ExternalSystemConfigRepository; +import org.avni.server.dao.individualRelationship.*; import org.avni.server.dao.program.SubjectProgramEligibilityRepository; import org.avni.server.dao.task.TaskRepository; +import org.avni.server.dao.task.TaskStatusRepository; +import org.avni.server.dao.task.TaskTypeRepository; +import org.avni.server.dao.task.TaskUnAssignmentRepository; import org.avni.server.domain.*; import org.avni.server.domain.accessControl.GroupPrivilege; import org.avni.server.framework.security.UserContextHolder; @@ -45,6 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -101,10 +102,12 @@ public class OrganisationService { private final CardRepository cardRepository; private final DashboardRepository dashboardRepository; private final DashboardSectionCardMappingRepository dashboardSectionCardMappingRepository; + private final DashboardFilterRepository dashboardFilterRepository; private final DashboardSectionRepository dashboardSectionRepository; private final GroupDashboardRepository groupDashboardRepository; private final Msg91ConfigRepository msg91ConfigRepository; private final S3Service s3Service; + private final UserService userService; //Tx repositories private final RuleFailureTelemetryRepository ruleFailureTelemetryRepository; @@ -140,6 +143,24 @@ public class OrganisationService { private final OrganisationRepository organisationRepository; private final UserSubjectRepository userSubjectRepository; private final Logger logger; + private final AnswerConceptMigrationRepository answerConceptMigrationRepository; + private final CustomQueryRepository customQueryRepository; + private final DocumentationRepository documentationRepository; + private final DocumentationItemRepository documentationItemRepository; + private final ExportJobParametersRepository exportJobParametersRepository; + private final ExternalSystemConfigRepository externalSystemConfigRepository; + private final LocationMappingRepository locationMappingRepository; + private final MenuItemRepository menuItemRepository; + private final ManualMessageRepository manualMessageRepository; + private final MessageRuleRepository messageRuleRepository; + private final ResetSyncRepository resetSyncRepository; + private final RuleFailureLogRepository ruleFailureLogRepository; + private final TaskStatusRepository taskStatusRepository; + private final TaskTypeRepository taskTypeRepository; + private final TaskUnAssignmentRepository taskUnAssignmentRepository; + private final UserRepository userRepository; + + private final JdbcTemplate jdbcTemplate; @Autowired public OrganisationService(FormRepository formRepository, @@ -182,10 +203,10 @@ public OrganisationService(FormRepository formRepository, CardRepository cardRepository, DashboardRepository dashboardRepository, DashboardSectionCardMappingRepository dashboardSectionCardMappingRepository, - DashboardSectionRepository dashboardSectionRepository, + DashboardFilterRepository dashboardFilterRepository, DashboardSectionRepository dashboardSectionRepository, GroupDashboardRepository groupDashboardRepository, Msg91ConfigRepository msg91ConfigRepository, - S3Service s3Service, RuleFailureTelemetryRepository ruleFailureTelemetryRepository, + S3Service s3Service, UserService userService, RuleFailureTelemetryRepository ruleFailureTelemetryRepository, IdentifierAssignmentRepository identifierAssignmentRepository, SyncTelemetryRepository syncTelemetryRepository, VideoTelemetricRepository videoTelemetricRepository, @@ -216,7 +237,24 @@ public OrganisationService(FormRepository formRepository, OrganisationConfigService organisationConfigService, GenderRepository genderRepository, OrganisationRepository organisationRepository, - UserSubjectRepository userSubjectRepository) { + UserSubjectRepository userSubjectRepository, + AnswerConceptMigrationRepository answerConceptMigrationRepository, + CustomQueryRepository customQueryRepository, + DocumentationRepository documentationRepository, + DocumentationItemRepository documentationItemRepository, + ExportJobParametersRepository exportJobParametersRepository, + ExternalSystemConfigRepository externalSystemConfigRepository, + LocationMappingRepository locationMappingRepository, + MenuItemRepository menuItemRepository, + ManualMessageRepository manualMessageRepository, + MessageRuleRepository messageRuleRepository, + ResetSyncRepository resetSyncRepository, + RuleFailureLogRepository ruleFailureLogRepository, + TaskStatusRepository taskStatusRepository, + TaskTypeRepository taskTypeRepository, + TaskUnAssignmentRepository taskUnAssignmentRepository, + UserRepository userRepository, + JdbcTemplate jdbcTemplate) { this.formRepository = formRepository; this.addressLevelTypeRepository = addressLevelTypeRepository; this.locationRepository = locationRepository; @@ -257,10 +295,12 @@ public OrganisationService(FormRepository formRepository, this.cardRepository = cardRepository; this.dashboardRepository = dashboardRepository; this.dashboardSectionCardMappingRepository = dashboardSectionCardMappingRepository; + this.dashboardFilterRepository = dashboardFilterRepository; this.dashboardSectionRepository = dashboardSectionRepository; this.groupDashboardRepository = groupDashboardRepository; this.msg91ConfigRepository = msg91ConfigRepository; this.s3Service = s3Service; + this.userService = userService; this.ruleFailureTelemetryRepository = ruleFailureTelemetryRepository; this.identifierAssignmentRepository = identifierAssignmentRepository; this.syncTelemetryRepository = syncTelemetryRepository; @@ -293,6 +333,23 @@ public OrganisationService(FormRepository formRepository, this.genderRepository = genderRepository; this.organisationRepository = organisationRepository; this.userSubjectRepository = userSubjectRepository; + this.answerConceptMigrationRepository = answerConceptMigrationRepository; + this.customQueryRepository = customQueryRepository; + this.documentationRepository = documentationRepository; + this.documentationItemRepository = documentationItemRepository; + this.exportJobParametersRepository = exportJobParametersRepository; + this.externalSystemConfigRepository = externalSystemConfigRepository; + this.locationMappingRepository = locationMappingRepository; + this.menuItemRepository = menuItemRepository; + this.manualMessageRepository = manualMessageRepository; + this.messageRuleRepository = messageRuleRepository; + this.resetSyncRepository = resetSyncRepository; + this.ruleFailureLogRepository = ruleFailureLogRepository; + this.taskStatusRepository = taskStatusRepository; + this.taskTypeRepository = taskTypeRepository; + this.taskUnAssignmentRepository = taskUnAssignmentRepository; + this.userRepository = userRepository; + this.jdbcTemplate = jdbcTemplate; logger = LoggerFactory.getLogger(this.getClass()); } @@ -657,7 +714,8 @@ private void addDirectoryToZip(ZipOutputStream zos, String directoryName) throws } - public void deleteTransactionalData() { + public void deleteTransactionalData(Organisation organisation) { + deleteNonRepositoryTransactionalData(organisation); JpaRepository[] transactionalRepositories = { newsRepository, commentRepository, @@ -677,21 +735,27 @@ public void deleteTransactionalData() { subjectMigrationRepository, userSubjectAssignmentRepository, subjectProgramEligibilityRepository, + taskUnAssignmentRepository, taskRepository, userSubjectRepository, - individualRepository + individualRepository, + resetSyncRepository, }; CrudRepository[] txCrudRepositories = { + exportJobParametersRepository, + manualMessageRepository, messageReceiverRepository, messageRequestQueueRepository, + ruleFailureLogRepository, }; Arrays.asList(txCrudRepositories).forEach(this::deleteAll); Arrays.asList(transactionalRepositories).forEach(this::deleteAll); } - public void deleteMetadata() { + public void deleteMetadata(Organisation organisation) { + deleteNonRepositoryMetadata(organisation); JpaRepository[] metadataRepositories = { groupPrivilegeRepository, groupRoleRepository, @@ -700,11 +764,11 @@ public void deleteMetadata() { identifierUserAssignmentRepository, identifierSourceRepository, individualRelationGenderMappingRepository, - individualRelationshipTypeRepository, individualRelationRepository, - formMappingRepository, + individualRelationshipTypeRepository, formElementRepository, formElementGroupRepository, + formMappingRepository, formRepository, conceptAnswerRepository, conceptRepository, @@ -725,10 +789,90 @@ public void deleteMetadata() { msg91ConfigRepository, genderRepository, userGroupRepository, - groupRepository + groupRepository, + answerConceptMigrationRepository, + dashboardFilterRepository, + documentationRepository, + documentationItemRepository, + menuItemRepository, + ruleRepository, + ruleDependencyRepository, + taskTypeRepository, + taskStatusRepository, + translationRepository, + userSubjectAssignmentRepository, + }; + + CrudRepository[] metadataCrudRepositories = { + messageRuleRepository, + customQueryRepository, }; Arrays.asList(metadataRepositories).forEach(this::deleteAll); + Arrays.asList(metadataCrudRepositories).forEach(this::deleteAll); + + } + +public void deleteNonRepositoryTransactionalData(Organisation organisation) { + String individualRelativeDeletionQuery = "delete from individual_relative where organisation_id = %d and organisation_id > 1"; + jdbcTemplate.execute(String.format(individualRelativeDeletionQuery, organisation.getId())); +} + + public void deleteNonRepositoryMetadata(Organisation organisation) { + String decisionConceptsDeletionQuery = "delete from decision_concept dc using concept c where dc.concept_id = c.id and c.organisation_id = %d and c.organisation_id > 1"; + String nonApplicableFormElementDeletionQuery = "delete from non_applicable_form_element where organisation_id = %d and organisation_id > 1"; + jdbcTemplate.execute(String.format(decisionConceptsDeletionQuery, organisation.getId())); + jdbcTemplate.execute(String.format(nonApplicableFormElementDeletionQuery, organisation.getId())); + } + + public void deleteAdminConfigData(Organisation organisation) { + deleteNonRepositoryAdminConfigData(organisation); + JpaRepository[] adminConfigRepositories = { + catchmentRepository, + locationMappingRepository, + locationRepository, + addressLevelTypeRepository, + msg91ConfigRepository, + +// organisationCategoryRepository, // Not org specific +// organisationStatusRepository, // Not org specific +// organisationGroupOrganisationRepository, // Deleting orgGroup Org is out of scope +// organisationGroupRepository, // Deleting orgGroup is out of scope +// organisationRepository, // Deleting org itself is out of scope + +// batch_job_instance // Not org specific +// batch_job_execution // Not org specific +// batch_step_execution // Not org specific +// batch_job_execution_context // Not org specific +// batch_job_execution_params // Not org specific +// batch_step_execution_context // Not org specific + +// audit //Deprecated +// facility //Deprecated +// program_outcome //Deprecated +// dashboard_card_mapping //Deprecated +// deps_saved_ddl // Not org specific +// schema_version // Not org specific +// scheduled_job_run // Not org specific +// privilegeRepository, // Not org specific +// flyway_schema_history // Not org specific +// approvalStatusRepository, // Not org specific +// platformTranslationRepository, // Not org specific +// standardReportCardTypeRepository, // Not org specific + }; + + CrudRepository[] adminConfigCrudRepositories = { + externalSystemConfigRepository, + }; + + Arrays.asList(adminConfigRepositories).forEach(this::deleteAll); + Arrays.asList(adminConfigCrudRepositories).forEach(this::deleteAll); + userRepository.findAllByOrganisationId(organisation.getId()).stream().filter(user -> !user.hasAllPrivileges()).forEach(user -> userService.deleteUser(user.getId())); + } + + public void deleteNonRepositoryAdminConfigData(Organisation organisation) { + String catchmentAddressMappingDeletionQuery = "delete from catchment_address_mapping cam using address_level al where cam.addresslevel_id = al.id and al.organisation_id = %d and al.organisation_id > 1)"; + jdbcTemplate.execute(String.format(catchmentAddressMappingDeletionQuery, organisation.getId().toString())); } private void deleteAll(JpaRepository repository) { @@ -744,6 +888,14 @@ public void deleteMediaContent(boolean deleteMetadata) { logger.info("Error while deleting the media files, skipping."); } } + public void deleteETLData(Organisation organisation) { + String baseQuery = "select delete_etl_metadata_for_schema('$impl_schema', '$impl_db_user', '$impl_db_owner')"; + String query = baseQuery + .replace("$impl_schema", organisation.getSchemaName()) + .replace("$impl_db_user", organisation.getDbUser()) + .replace("$impl_db_owner", organisation.getDbUser()); + jdbcTemplate.execute(query); + } public void addGroupDashboardJson(ZipOutputStream zos) throws IOException { List groupDashboards = groupDashboardRepository.findAll().stream() diff --git a/avni-server-api/src/main/java/org/avni/server/service/StorageService.java b/avni-server-api/src/main/java/org/avni/server/service/StorageService.java index 0acc845e0..666fd3e8b 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/StorageService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/StorageService.java @@ -13,6 +13,7 @@ import org.avni.server.domain.Organisation; import org.avni.server.domain.OrganisationConfig; import org.avni.server.domain.UserContext; +import org.avni.server.domain.accessControl.PrivilegeType; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.util.AvniFiles; import org.avni.server.util.S; diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserService.java b/avni-server-api/src/main/java/org/avni/server/service/UserService.java index 91345d47d..320c5090f 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserService.java @@ -1,16 +1,20 @@ package org.avni.server.service; +import com.amazonaws.services.cognitoidp.model.AWSCognitoIdentityProviderException; import org.avni.server.application.Subject; import org.avni.server.dao.*; import org.avni.server.domain.*; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.exception.GroupNotFoundException; +import org.avni.server.util.WebResponseUtil; import org.avni.server.web.validation.ValidationException; import org.bouncycastle.util.Strings; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -32,15 +36,17 @@ public class UserService implements NonScopeAwareService { private final UserSubjectRepository userSubjectRepository; private final IndividualRepository individualRepository; private final SubjectTypeRepository subjectTypeRepository; + private final IdpServiceFactory idpServiceFactory; @Autowired - public UserService(UserRepository userRepository, GroupRepository groupRepository, UserGroupRepository userGroupRepository, UserSubjectRepository userSubjectRepository, IndividualRepository individualRepository, SubjectTypeRepository subjectTypeRepository) { + public UserService(UserRepository userRepository, GroupRepository groupRepository, UserGroupRepository userGroupRepository, UserSubjectRepository userSubjectRepository, IndividualRepository individualRepository, SubjectTypeRepository subjectTypeRepository, IdpServiceFactory idpServiceFactory) { this.userRepository = userRepository; this.groupRepository = groupRepository; this.userGroupRepository = userGroupRepository; this.userSubjectRepository = userSubjectRepository; this.individualRepository = individualRepository; this.subjectTypeRepository = subjectTypeRepository; + this.idpServiceFactory = idpServiceFactory; } public User getCurrentUser() { @@ -187,4 +193,18 @@ public void ensureSubjectForUser(User user, SubjectType subjectType) { individualRepository.save(subject); userSubjectRepository.save(userSubject); } + + public ResponseEntity deleteUser(Long id) { + try { + User user = userRepository.findOne(id); + idpServiceFactory.getIdpService(user).deleteUser(user); + user.setVoided(true); + user.setDisabledInCognito(true); + userRepository.save(user); + logger.info(String.format("Deleted user '%s', UUID '%s'", user.getUsername(), user.getUuid())); + return new ResponseEntity<>(user, HttpStatus.CREATED); + } catch (AWSCognitoIdentityProviderException ex) { + return WebResponseUtil.createInternalServerErrorResponse(ex, logger); + } + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java index ac98b5080..5bbbc24a3 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java @@ -23,8 +23,6 @@ import java.io.ByteArrayOutputStream; import java.util.zip.ZipOutputStream; -import static org.avni.server.domain.accessControl.PrivilegeType.MultiTxEntityTypeUpdate; - @RestController public class ImplementationController implements RestControllerResourceProcessor { @@ -104,7 +102,8 @@ public ResponseEntity export(@PathVariable boolean includeLoc @RequestMapping(value = "/implementation/delete", method = RequestMethod.DELETE) @Transactional - public ResponseEntity delete(@Param("deleteMetadata") boolean deleteMetadata) { + public ResponseEntity delete(@Param("deleteMetadata") boolean deleteMetadata, + @Param("deleteAdminConfig") boolean deleteAdminConfig) { if (accessControlService.isSuperAdmin()) { return new ResponseEntity<>("Super admin cannot delete implementation data", HttpStatus.FORBIDDEN); } @@ -113,19 +112,49 @@ public ResponseEntity delete(@Param("deleteMetadata") boolean deleteMetadata) { return new ResponseEntity<>("Production organisation's data cannot be deleted", HttpStatus.CONFLICT); } - accessControlService.checkPrivilege(MultiTxEntityTypeUpdate); - organisationService.deleteTransactionalData(); + checkPrivilegeAndDeleteTransactionalData(organisation); + checkPrivilegeAndDeleteMetadata(deleteMetadata, organisation); + selectivelyCleanupMediaContent(deleteMetadata); + checkPrivilegeAndDeleteAdminConfigurationData(deleteAdminConfig, organisation); + checkPrivilegeAndRecreateBasicMetadata(deleteMetadata); + + return new ResponseEntity<>(HttpStatus.OK); + } + + private void checkPrivilegeAndRecreateBasicMetadata(boolean deleteMetadata) { + if (deleteMetadata) { + accessControlService.checkPrivilege(PrivilegeType.UploadMetadataAndData); + organisationService.setupBaseOrganisationData(UserContextHolder.getOrganisation()); + } + } + + private void checkPrivilegeAndDeleteAdminConfigurationData(boolean deleteAdminConfig, Organisation organisation) { + if(deleteAdminConfig){ + accessControlService.checkPrivilege(PrivilegeType.DeleteOrganisationConfiguration); + organisationService.deleteAdminConfigData(organisation); + } + } + private void selectivelyCleanupMediaContent(boolean deleteMetadata) { if (deleteMetadata) { - accessControlService.checkPrivilege(PrivilegeType.DownloadBundle); - organisationService.deleteMetadata(); + accessControlService.checkPrivilege(PrivilegeType.UploadMetadataAndData); + } else { + accessControlService.checkPrivilege(PrivilegeType.EditOrganisationConfiguration); } organisationService.deleteMediaContent(deleteMetadata); + } - if (deleteMetadata) - organisationService.setupBaseOrganisationData(UserContextHolder.getOrganisation()); + private void checkPrivilegeAndDeleteMetadata(boolean deleteMetadata, Organisation organisation) { + if (deleteMetadata) { + accessControlService.checkPrivilege(PrivilegeType.UploadMetadataAndData); + organisationService.deleteMetadata(organisation); + organisationService.deleteETLData(organisation); + } + } - return new ResponseEntity<>(HttpStatus.OK); + private void checkPrivilegeAndDeleteTransactionalData(Organisation organisation) { + accessControlService.checkPrivilege(PrivilegeType.EditOrganisationConfiguration); + organisationService.deleteTransactionalData(organisation); } private HttpHeaders getHttpHeaders() { diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserController.java b/avni-server-api/src/main/java/org/avni/server/web/UserController.java index 038f547e0..0b17619a2 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserController.java @@ -208,17 +208,7 @@ private User setUserAttributes(User user, UserContract userContract) { @Transactional public ResponseEntity deleteUser(@PathVariable("id") Long id) { accessControlService.checkPrivilege(PrivilegeType.EditUserConfiguration); - try { - User user = userRepository.findOne(id); - idpServiceFactory.getIdpService(user).deleteUser(user); - user.setVoided(true); - user.setDisabledInCognito(true); - userRepository.save(user); - logger.info(String.format("Deleted user '%s', UUID '%s'", user.getUsername(), user.getUuid())); - return new ResponseEntity<>(user, HttpStatus.CREATED); - } catch (AWSCognitoIdentityProviderException ex) { - return WebResponseUtil.createInternalServerErrorResponse(ex, logger); - } + return userService.deleteUser(id); } @RequestMapping(value = {"/user/{id}/disable", "/user/accountOrgAdmin/{id}/disable"}, method = RequestMethod.PUT) diff --git a/avni-server-api/src/test/java/org/avni/server/service/UserServiceAssociateGroupsTest.java b/avni-server-api/src/test/java/org/avni/server/service/UserServiceAssociateGroupsTest.java index c683525de..72dde7328 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/UserServiceAssociateGroupsTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/UserServiceAssociateGroupsTest.java @@ -30,6 +30,9 @@ public class UserServiceAssociateGroupsTest { private UserGroupRepository userGroupRepository; @Mock private GroupRepository groupRepository; + @Mock + private IdpServiceFactory idpServiceFactory; + @Captor ArgumentCaptor userGroupArgumentCaptor; @Captor @@ -50,7 +53,7 @@ public class UserServiceAssociateGroupsTest { public void setup() { initMocks(this); - userService = new UserService(null, groupRepository, userGroupRepository, null, null, null); + userService = new UserService(null, groupRepository, userGroupRepository, null, null, null, idpServiceFactory); // init orgId = 1234l; diff --git a/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java index 52c5f5f38..98fe998d3 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java @@ -23,6 +23,8 @@ public class UserServiceTest { private UserGroupRepository userGroupRepository; @Mock private GroupRepository groupRepository; + @Mock + private IdpServiceFactory idpServiceFactory; @Captor ArgumentCaptor userGroupArgumentCaptor; @@ -32,7 +34,7 @@ public class UserServiceTest { public void setup() { initMocks(this); - userService = new UserService(null, groupRepository, userGroupRepository, null, null, null); + userService = new UserService(null, groupRepository, userGroupRepository, null, null, null, idpServiceFactory); } @Test From 0624f827dc341b9eb9fb87f5ccd5a8686f51bfb5 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 12 Aug 2024 13:14:19 +0530 Subject: [PATCH 075/129] #750 - do not use isAdmin wherever possible as it doesn't work during using creation, updates. replaced in some safe places. --- .../main/java/org/avni/server/domain/User.java | 9 +++++++++ .../server/framework/security/AuthService.java | 8 +++++--- .../avni/server/service/IdpServiceFactory.java | 10 ++++++++-- .../org/avni/server/service/UserService.java | 9 ++++++++- .../accessControl/AccessControlService.java | 7 +------ .../server/web/ImplementationController.java | 8 +++++--- .../avni/server/web/UserInfoController.java | 5 +++-- .../avni/server/web/UserInfoWebController.java | 8 ++++++-- .../avni/server/service/AuthServiceTest.java | 18 +++++++----------- .../UserServiceAssociateGroupsTest.java | 2 +- .../avni/server/service/UserServiceTest.java | 2 +- 11 files changed, 54 insertions(+), 32 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/User.java b/avni-server-api/src/main/java/org/avni/server/domain/User.java index 19c5c7f32..d08b8cf1e 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/User.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/User.java @@ -82,6 +82,9 @@ public class User { private Set accountAdmin = new HashSet<>(); @Transient + /* + Using transient field isAdmin is problematic when the user is not the current user. User UserService.isAdmin() instead, except in JDBC interceptor. + */ private boolean isAdmin; @JsonIgnore @@ -309,10 +312,16 @@ public void setAccountAdmin(AccountAdmin accountAdmin) { } } + /** + * Using transient field isAdmin is problematic when the user is not the current user. User UserService.isAdmin() instead, except in JDBC interceptor. + */ public boolean isAdmin() { return isAdmin; } + /** + * Using transient field isAdmin is problematic when the user is not the current user. User UserService.isAdmin() instead, except in JDBC interceptor. + */ public void setAdmin(boolean admin) { this.isAdmin = admin; } diff --git a/avni-server-api/src/main/java/org/avni/server/framework/security/AuthService.java b/avni-server-api/src/main/java/org/avni/server/framework/security/AuthService.java index 6c135bc4f..2a24ba034 100644 --- a/avni-server-api/src/main/java/org/avni/server/framework/security/AuthService.java +++ b/avni-server-api/src/main/java/org/avni/server/framework/security/AuthService.java @@ -12,6 +12,7 @@ import org.avni.server.domain.accessControl.AvniNoUserSessionException; import org.avni.server.service.IAMAuthService; import org.avni.server.service.IdpServiceFactory; +import org.avni.server.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; @@ -29,14 +30,16 @@ public class AuthService { private final UserRepository userRepository; private final OrganisationRepository organisationRepository; private final AccountAdminRepository accountAdminRepository; + private final UserService userService; private final IdpServiceFactory idpServiceFactory; @Autowired - public AuthService(UserRepository userRepository, OrganisationRepository organisationRepository, AccountAdminRepository accountAdminRepository, IdpServiceFactory idpServiceFactory) { + public AuthService(UserRepository userRepository, OrganisationRepository organisationRepository, AccountAdminRepository accountAdminRepository, IdpServiceFactory idpServiceFactory, UserService userService) { this.idpServiceFactory = idpServiceFactory; this.userRepository = userRepository; this.organisationRepository = organisationRepository; this.accountAdminRepository = accountAdminRepository; + this.userService = userService; } public UserContext authenticateByUserName(String username, String organisationUUID) { @@ -81,8 +84,7 @@ private Authentication attemptAuthentication(User user, String organisationUUID) if (user == null) { return null; } - List accountAdmins = accountAdminRepository.findByUser_Id(user.getId()); - user.setAdmin(accountAdmins.size() > 0); + user.setAdmin(userService.isAdmin(user)); Organisation organisation = null; if (organisationUUID != null) { organisation = organisationRepository.findByUuid(organisationUUID); diff --git a/avni-server-api/src/main/java/org/avni/server/service/IdpServiceFactory.java b/avni-server-api/src/main/java/org/avni/server/service/IdpServiceFactory.java index dd986ac1b..a0cb91f12 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/IdpServiceFactory.java +++ b/avni-server-api/src/main/java/org/avni/server/service/IdpServiceFactory.java @@ -35,11 +35,13 @@ public class IdpServiceFactory { @Autowired private OrganisationConfigService organisationConfigService; + @Autowired + private UserService userService; public IdpServiceFactory() { } - public IdpServiceFactory(OrganisationRepository organisationRepository, CognitoIdpService cognitoIdpService, KeycloakIdpService keycloakIdpService, CognitoAuthServiceImpl cognitoAuthService, KeycloakAuthService keycloakAuthService, IdpType idpType, OrganisationConfigService organisationConfigService) { + public IdpServiceFactory(OrganisationRepository organisationRepository, CognitoIdpService cognitoIdpService, KeycloakIdpService keycloakIdpService, CognitoAuthServiceImpl cognitoAuthService, KeycloakAuthService keycloakAuthService, IdpType idpType, OrganisationConfigService organisationConfigService, UserService userService) { this.organisationRepository = organisationRepository; this.cognitoIdpService = cognitoIdpService; this.keycloakIdpService = keycloakIdpService; @@ -47,6 +49,7 @@ public IdpServiceFactory(OrganisationRepository organisationRepository, CognitoI this.keycloakAuthService = keycloakAuthService; this.idpType = idpType; this.organisationConfigService = organisationConfigService; + this.userService = userService; } public IdpService getIdpService() { @@ -72,6 +75,9 @@ public IdpService getIdpService(Organisation organisation) { } public IdpService getIdpService(User user) { + if (userService.isAdmin(user)) { + return getIdpService(); + } Organisation organisation = organisationRepository.findOne(user.getOrganisationId()); return getIdpService(organisation); } @@ -121,4 +127,4 @@ public User getUserFromToken(String token) throws SigningKeyNotFoundException { return null; } } -} \ No newline at end of file +} diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserService.java b/avni-server-api/src/main/java/org/avni/server/service/UserService.java index 8aca6449e..e51822a50 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserService.java @@ -32,15 +32,17 @@ public class UserService implements NonScopeAwareService { private final UserSubjectRepository userSubjectRepository; private final IndividualRepository individualRepository; private final SubjectTypeRepository subjectTypeRepository; + private final AccountAdminRepository accountAdminRepository; @Autowired - public UserService(UserRepository userRepository, GroupRepository groupRepository, UserGroupRepository userGroupRepository, UserSubjectRepository userSubjectRepository, IndividualRepository individualRepository, SubjectTypeRepository subjectTypeRepository) { + public UserService(UserRepository userRepository, GroupRepository groupRepository, UserGroupRepository userGroupRepository, UserSubjectRepository userSubjectRepository, IndividualRepository individualRepository, SubjectTypeRepository subjectTypeRepository, AccountAdminRepository accountAdminRepository) { this.userRepository = userRepository; this.groupRepository = groupRepository; this.userGroupRepository = userGroupRepository; this.userSubjectRepository = userSubjectRepository; this.individualRepository = individualRepository; this.subjectTypeRepository = subjectTypeRepository; + this.accountAdminRepository = accountAdminRepository; } public User getCurrentUser() { @@ -193,4 +195,9 @@ public void setPhoneNumber(String phoneNumber, User user, String userRegion) { } user.setPhoneNumber(PhoneNumberUtil.getStandardFormatPhoneNumber(phoneNumber, userRegion)); } + + public boolean isAdmin(User user) { + List accountAdmins = accountAdminRepository.findByUser_Id(user.getId()); + return !accountAdmins.isEmpty(); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java index 84e258f7f..de77de436 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/AccessControlService.java @@ -186,15 +186,10 @@ public void checkEncounterPrivileges(PrivilegeType privilegeType, List { - private final OrganisationService organisationService; private final AccessControlService accessControlService; + private final UserService userService; @Autowired - public ImplementationController(OrganisationService organisationService, AccessControlService accessControlService) { + public ImplementationController(OrganisationService organisationService, AccessControlService accessControlService, UserService userService) { this.organisationService = organisationService; this.accessControlService = accessControlService; + this.userService = userService; } @RequestMapping(value = "/implementation/export/{includeLocations}", method = RequestMethod.GET) @@ -105,7 +107,7 @@ public ResponseEntity export(@PathVariable boolean includeLoc @RequestMapping(value = "/implementation/delete", method = RequestMethod.DELETE) @Transactional public ResponseEntity delete(@Param("deleteMetadata") boolean deleteMetadata) { - if (accessControlService.isSuperAdmin()) { + if (userService.isAdmin(UserContextHolder.getUser())) { return new ResponseEntity<>("Super admin cannot delete implementation data", HttpStatus.FORBIDDEN); } Organisation organisation = organisationService.getCurrentOrganisation(); diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserInfoController.java b/avni-server-api/src/main/java/org/avni/server/web/UserInfoController.java index 08b39e1a7..aa876f1c3 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserInfoController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserInfoController.java @@ -67,12 +67,13 @@ public ResponseEntity getUserInfo() { UserContext userContext = UserContextHolder.getUserContext(); User user = userContext.getUser(); Organisation organisation = userContext.getOrganisation(); + boolean isAdmin = userService.isAdmin(user); - if (organisation == null && !user.isAdmin()) { + if (organisation == null && !isAdmin) { logger.info(String.format("Organisation not found for user ID: %s", user.getId())); return new ResponseEntity<>(new UserInfoClientContract(), HttpStatus.NOT_FOUND); } - if (user.isAdmin() && organisation == null) { + if (isAdmin && organisation == null) { organisation = new Organisation(); } return new ResponseEntity<>(getUserInfoObject(organisation, user), HttpStatus.OK); diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserInfoWebController.java b/avni-server-api/src/main/java/org/avni/server/web/UserInfoWebController.java index 2a0627958..b16ca4f90 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserInfoWebController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserInfoWebController.java @@ -8,6 +8,7 @@ import org.avni.server.domain.accessControl.Privilege; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.IdpServiceFactory; +import org.avni.server.service.UserService; import org.avni.server.service.accessControl.GroupPrivilegeService; import org.avni.server.web.response.UserPrivilegeWebResponse; import org.avni.server.web.response.UserInfoWebResponse; @@ -25,13 +26,15 @@ public class UserInfoWebController { private final UserRepository userRepository; private final PrivilegeRepository privilegeRepository; private final IdpServiceFactory idpServiceFactory; + private final UserService userService; @Autowired - public UserInfoWebController(GroupPrivilegeService groupPrivilegeService, UserRepository userRepository, PrivilegeRepository privilegeRepository, IdpServiceFactory idpServiceFactory) { + public UserInfoWebController(GroupPrivilegeService groupPrivilegeService, UserRepository userRepository, PrivilegeRepository privilegeRepository, IdpServiceFactory idpServiceFactory, UserService userService) { this.groupPrivilegeService = groupPrivilegeService; this.userRepository = userRepository; this.privilegeRepository = privilegeRepository; this.idpServiceFactory = idpServiceFactory; + this.userService = userService; } @RequestMapping(value = "/web/userInfo", method = RequestMethod.GET) @@ -39,7 +42,8 @@ public UserInfoWebResponse getUserInfo() { Organisation contextOrganisation = UserContextHolder.getOrganisation(); User contextUser = UserContextHolder.getUser(); User user = userRepository.findOne(contextUser.getId()); - if (UserContextHolder.getUser().isAdmin()) { + + if (userService.isAdmin(user)) { List adminPrivileges = privilegeRepository.getAdminPrivileges(); List groupPrivilegeResponses = adminPrivileges.stream() .map(UserPrivilegeWebResponse::createForAdminUser).collect(Collectors.toList()); diff --git a/avni-server-api/src/test/java/org/avni/server/service/AuthServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/AuthServiceTest.java index 73cd875bb..a9a16f1ce 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/AuthServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/AuthServiceTest.java @@ -27,6 +27,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.core.Is.is; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -41,6 +42,8 @@ public class AuthServiceTest { private CognitoAuthServiceImpl cognitoAuthService; @Mock private AccountAdminRepository accountAdminRepository; + @Mock + private UserService userService; private User user; private AuthService authService; private AccountAdmin accountAdmin; @@ -48,9 +51,8 @@ public class AuthServiceTest { @Before public void setup() { initMocks(this); -// cognitoAuthService = new CognitoUserContextServiceImpl(organisationRepository, userRepository, "poolId", "clientId"); authService = new AuthService(userRepository, organisationRepository, accountAdminRepository, - new IdpServiceFactory(organisationRepository, null, null, cognitoAuthService, keycloakAuthService, IdpType.cognito, null)); + new IdpServiceFactory(organisationRepository, null, null, cognitoAuthService, keycloakAuthService, IdpType.cognito, null, userService), userService); String uuid = "9ecc2805-6528-47ee-8267-9368b266ad39"; user = new User(); user.setUuid(uuid); @@ -77,16 +79,10 @@ public void shouldAddOrganisationToContext() throws SigningKeyNotFoundException when(organisationRepository.findOne(1L)).thenReturn(organisation); when(userRepository.findByUuid(user.getUuid())).thenReturn(user); when(cognitoAuthService.getUserFromToken("some token")).thenReturn(user); -// Algorithm algorithm = Algorithm.HMAC256("not very useful secret"); -// String token = createForBaseToken(user.getUuid()).sign(algorithm); UserContext userContext = authService.authenticateByToken("some token", null); assertThat(userContext.getOrganisation(), is(equalTo(organisation))); } - private JWTCreator.Builder createForBaseToken(String userUuid) { - return JWT.create().withClaim("custom:userUUID", userUuid); - } - @Test public void shouldAddRolesToContext() throws SigningKeyNotFoundException { Organisation organisation = new Organisation(); @@ -106,18 +102,18 @@ public void shouldAddRolesToContext() throws SigningKeyNotFoundException { assertThat(userContext.getRoles(), containsInAnyOrder(User.USER)); user.setAccountAdmin(accountAdmin); - when(accountAdminRepository.findByUser_Id(user.getId())).thenReturn(adminUser); + when(userService.isAdmin(any())).thenReturn(true); userContext = authService.authenticateByToken("some token", null); assertThat(userContext.getRoles().size(), is(equalTo(0))); user.setAccountAdmin(null); - when(accountAdminRepository.findByUser_Id(user.getId())).thenReturn(new ArrayList<>()); + when(userService.isAdmin(any())).thenReturn(false); userContext = authService.authenticateByToken("some token", null); assertThat(userContext.getRoles().size(), is(equalTo(1))); assertThat(userContext.getRoles(), contains(User.USER)); user.setAccountAdmin(accountAdmin); - when(accountAdminRepository.findByUser_Id(user.getId())).thenReturn(adminUser); + when(userService.isAdmin(any())).thenReturn(true); userContext = authService.authenticateByToken("some token", null); assertThat(userContext.getRoles().size(), is(equalTo(0))); } diff --git a/avni-server-api/src/test/java/org/avni/server/service/UserServiceAssociateGroupsTest.java b/avni-server-api/src/test/java/org/avni/server/service/UserServiceAssociateGroupsTest.java index c683525de..e52c72339 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/UserServiceAssociateGroupsTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/UserServiceAssociateGroupsTest.java @@ -50,7 +50,7 @@ public class UserServiceAssociateGroupsTest { public void setup() { initMocks(this); - userService = new UserService(null, groupRepository, userGroupRepository, null, null, null); + userService = new UserService(null, groupRepository, userGroupRepository, null, null, null, null); // init orgId = 1234l; diff --git a/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java index 2fc800aee..43c2fe597 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java @@ -32,7 +32,7 @@ public class UserServiceTest { public void setup() { initMocks(this); - userService = new UserService(null, groupRepository, userGroupRepository, null, null, null); + userService = new UserService(null, groupRepository, userGroupRepository, null, null, null, null); } @Test From 29f3e06d4f5b76f1f35dda54aad13994380eb5be Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 12 Aug 2024 17:15:51 +0530 Subject: [PATCH 076/129] #764 - filter out voided report cards along with report card mapping. --- .../server/importer/batch/zip/BundleZipFileImporter.java | 7 +------ .../org/avni/server/mapper/dashboard/DashboardMapper.java | 6 ++++-- .../java/org/avni/server/service/AddressLevelCache.java | 3 +-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java index 4560368f8..de251fd93 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java @@ -2,16 +2,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.avni.messaging.contract.MessageRuleContract; -import org.avni.messaging.domain.MessageRule; import org.avni.messaging.service.MessagingService; -import org.avni.server.application.Subject; -import org.avni.server.application.menu.MenuItem; import org.avni.server.builder.BuilderException; import org.avni.server.builder.FormBuilderException; import org.avni.server.dao.CardRepository; import org.avni.server.dao.SubjectTypeRepository; -import org.avni.server.domain.*; import org.avni.server.domain.Locale; +import org.avni.server.domain.*; import org.avni.server.framework.security.AuthService; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.importer.batch.model.BundleFile; @@ -47,8 +44,6 @@ import java.util.*; import java.util.stream.Collectors; -import static java.lang.String.format; - @Component @JobScope public class BundleZipFileImporter implements ItemWriter { diff --git a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardMapper.java b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardMapper.java index 531b0a963..291f25921 100644 --- a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardMapper.java +++ b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardMapper.java @@ -82,8 +82,10 @@ private DashboardSectionBundleContract toBundle(DashboardSection ds) { return dashboardContract; } - private void setDashboardSectionCardMappings(DashboardSectionWebResponse response, DashboardSection ds) { - List mappingContracts = ds.getDashboardSectionCardMappings().stream() + private void setDashboardSectionCardMappings(DashboardSectionWebResponse response, DashboardSection dashboardSection) { + List mappingContracts = dashboardSection.getDashboardSectionCardMappings() + .stream() + .filter(dashboardSectionCardMapping -> !dashboardSectionCardMapping.isVoided() && !dashboardSectionCardMapping.getCard().isVoided()) .map(dashboardSectionCardMapping -> { DashboardSectionCardMappingWebResponse mappingResponse = new DashboardSectionCardMappingWebResponse(); mappingResponse.setUuid(dashboardSectionCardMapping.getUuid()); diff --git a/avni-server-api/src/main/java/org/avni/server/service/AddressLevelCache.java b/avni-server-api/src/main/java/org/avni/server/service/AddressLevelCache.java index 15f6cb6f5..c6a2588b6 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/AddressLevelCache.java +++ b/avni-server-api/src/main/java/org/avni/server/service/AddressLevelCache.java @@ -12,8 +12,7 @@ @Component public class AddressLevelCache { public static final String ADDRESSES_PER_CATCHMENT = "addressesPerCatchment"; - public static final String ADDRESSES_PER_CATCHMENT_AND_MATCHING_ADDR_LEVELS = "addressesPerCatchmentAndMatchingAddrLevels" + - "";; + public static final String ADDRESSES_PER_CATCHMENT_AND_MATCHING_ADDR_LEVELS = "addressesPerCatchmentAndMatchingAddrLevels"; private final LocationRepository locationRepository; From 5de7e99a6eea8fcce57444150f0def689241fb4d Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 13 Aug 2024 10:07:48 +0530 Subject: [PATCH 077/129] avniproject/avni-webapp#1306 - minor code cleanup --- .../batch/csv/writer/EncounterWriter.java | 22 +++++++------- .../batch/csv/writer/LocationWriter.java | 29 +++++++++---------- .../avni/server/service/ImportService.java | 8 ++--- .../batch/csv/writer/LocationWriterTest.java | 9 ++---- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/EncounterWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/EncounterWriter.java index 9afc6c26a..2a5bdea8d 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/EncounterWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/EncounterWriter.java @@ -26,17 +26,17 @@ @Component public class EncounterWriter extends EntityWriter implements ItemWriter, Serializable { - private EncounterRepository encounterRepository; - private IndividualRepository individualRepository; - private BasicEncounterCreator basicEncounterCreator; - private FormMappingRepository formMappingRepository; - private ObservationService observationService; - private RuleServerInvoker ruleServerInvoker; - private VisitCreator visitCreator; - private DecisionCreator decisionCreator; - private ObservationCreator observationCreator; - private EncounterService encounterService; - private EntityApprovalStatusWriter entityApprovalStatusWriter; + private final EncounterRepository encounterRepository; + private final IndividualRepository individualRepository; + private final BasicEncounterCreator basicEncounterCreator; + private final FormMappingRepository formMappingRepository; + private final ObservationService observationService; + private final RuleServerInvoker ruleServerInvoker; + private final VisitCreator visitCreator; + private final DecisionCreator decisionCreator; + private final ObservationCreator observationCreator; + private final EncounterService encounterService; + private final EntityApprovalStatusWriter entityApprovalStatusWriter; @Autowired public EncounterWriter(EncounterRepository encounterRepository, diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java index c0a28a9f8..88391d181 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java @@ -32,7 +32,6 @@ @StepScope @Component public class LocationWriter implements ItemWriter { - private static final LocationHeaders headers = new LocationHeaders(); @Value("#{jobParameters['locationUploadMode']}") private String locationUploadMode; @@ -71,7 +70,7 @@ public void init() { } @Override - public void write(List rows) throws Exception { + public void write(List rows) { List allErrorMsgs = new ArrayList<>(); if (LocationUploadMode.isCreateMode(locationUploadMode)) { validateCreateModeHeaders(rows.get(0).getHeaders(), allErrorMsgs); @@ -80,14 +79,14 @@ public void write(List rows) throws Exception { } for (Row row : rows) { if (LocationUploadMode.isCreateMode(locationUploadMode)) { - createLocationWriter(row, allErrorMsgs); + createLocation(row, allErrorMsgs); } else { - editLocationWriter(row, allErrorMsgs); + editLocation(row, allErrorMsgs); } } } - private void editLocationWriter(Row row, List allErrorMsgs) throws Exception { + private void editLocation(Row row, List allErrorMsgs) { String existingLocationTitleLineage = row.get(ImportLocationsConstants.COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY); if (existingLocationTitleLineage.equalsIgnoreCase(ImportLocationsConstants.LOCATION_WITH_FULL_HIERARCHY_DESCRIPTION)) return; String newLocationParentTitleLineage = row.get(ImportLocationsConstants.COLUMN_NAME_PARENT_LOCATION_WITH_FULL_HIERARCHY); @@ -95,7 +94,7 @@ private void editLocationWriter(Row row, List allErrorMsgs) throws Excep Optional existingLocationAddressLevel = locationRepository.findByTitleLineageIgnoreCase(existingLocationTitleLineage); if (!existingLocationAddressLevel.isPresent()) { allErrorMsgs.add(String.format("Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'", existingLocationTitleLineage)); - throw new Exception(String.join(", ", allErrorMsgs)); + throw new RuntimeException(String.join(", ", allErrorMsgs)); } AddressLevel newLocationParentAddressLevel = null; @@ -108,7 +107,7 @@ private void editLocationWriter(Row row, List allErrorMsgs) throws Excep updateExistingLocation(existingLocationAddressLevel.get(), newLocationParentAddressLevel, row, allErrorMsgs); } - private void createLocationWriter(Row row, List allErrorMsgs) throws Exception { + private void createLocation(Row row, List allErrorMsgs) { AddressLevel parent = null; AddressLevel location = null; for (String header : row.getHeaders()) { @@ -128,14 +127,14 @@ private void updateLocationProperties(Row row, List allErrorMsgs, Addres locationRepository.save(location); } - private void validateCreateModeHeaders(String[] headers, List allErrorMsgs) throws Exception { + private void validateCreateModeHeaders(String[] headers, List allErrorMsgs) { List headerList = Arrays.asList(headers); List locationTypeHeaders = checkIfHeaderHasLocationTypesInOrderForHierarchy(this.locationHierarchy, headerList, allErrorMsgs); List additionalHeaders = new ArrayList<>(headerList.subList(locationTypeHeaders.size(), headerList.size())); checkIfHeaderRowHasUnknownHeaders(additionalHeaders, allErrorMsgs); } - private void validateEditModeHeaders(String[] headers, List allErrorMsgs) throws Exception { + private void validateEditModeHeaders(String[] headers, List allErrorMsgs) { List headerList = Arrays.asList(headers); if (!headerList.contains(ImportLocationsConstants.COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY)) { allErrorMsgs.add(String.format("'%s' is required", ImportLocationsConstants.COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY)); @@ -146,23 +145,23 @@ private void validateEditModeHeaders(String[] headers, List allErrorMsgs allErrorMsgs.add(String.format("At least one of '%s', '%s' or '%s' is required", ImportLocationsConstants.COLUMN_NAME_NEW_LOCATION_NAME, ImportLocationsConstants.COLUMN_NAME_GPS_COORDINATES, ImportLocationsConstants.COLUMN_NAME_PARENT_LOCATION_WITH_FULL_HIERARCHY)); } if (!allErrorMsgs.isEmpty()) { - throw new Exception(String.join(", ", allErrorMsgs)); + throw new RuntimeException(String.join(", ", allErrorMsgs)); } } - private List checkIfHeaderHasLocationTypesInOrderForHierarchy(String locationHierarchy, List headerList, List allErrorMsgs) throws Exception { + private List checkIfHeaderHasLocationTypesInOrderForHierarchy(String locationHierarchy, List headerList, List allErrorMsgs) { List locationTypeNamesForHierachy = importService.getAddressLevelTypesForCreateModeSingleHierarchy(locationHierarchy) .stream().map(AddressLevelType::getName).collect(Collectors.toList()); this.locationTypeNames = locationTypeNamesForHierachy; if (headerList.size() >= locationTypeNamesForHierachy.size() && !headerList.subList(0, locationTypeNamesForHierachy.size()).equals(locationTypeNamesForHierachy)) { allErrorMsgs.add("Location types missing or not in order in header for specified Location Hierarchy. Please refer to sample file for valid list of headers."); - throw new Exception(String.join(", ", allErrorMsgs)); + throw new RuntimeException(String.join(", ", allErrorMsgs)); } return locationTypeNamesForHierachy; } - private void checkIfHeaderRowHasUnknownHeaders(List additionalHeaders, List allErrorMsgs) throws Exception { + private void checkIfHeaderRowHasUnknownHeaders(List additionalHeaders, List allErrorMsgs) { additionalHeaders.removeIf(StringUtils::isEmpty); if (!additionalHeaders.isEmpty()) { List locationPropertyNames = formService.getFormElementNamesForLocationTypeForms() @@ -170,7 +169,7 @@ private void checkIfHeaderRowHasUnknownHeaders(List additionalHeaders, L locationPropertyNames.add(LocationHeaders.gpsCoordinates); if ((!locationPropertyNames.containsAll(additionalHeaders))) { allErrorMsgs.add("Unknown headers included in file. Please refer to sample file for valid list of headers."); - throw new Exception(String.join(", ", allErrorMsgs)); + throw new RuntimeException(String.join(", ", allErrorMsgs)); } } } @@ -196,7 +195,7 @@ private boolean isValidLocation(String header, Row row, List locationTyp return locationTypeNames.contains(header) && !S.isEmpty(row.get(header)); } - private void updateExistingLocation(AddressLevel location, AddressLevel newParent, Row row, List allErrorMsgs) throws Exception { + private void updateExistingLocation(AddressLevel location, AddressLevel newParent, Row row, List allErrorMsgs) { String newTitle = row.get(ImportLocationsConstants.COLUMN_NAME_NEW_LOCATION_NAME); if (!StringUtils.isEmpty(newTitle)) location.setTitle(newTitle); if (newParent != null) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/ImportService.java b/avni-server-api/src/main/java/org/avni/server/service/ImportService.java index 7a8e45c5d..dfe623f4d 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ImportService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ImportService.java @@ -185,9 +185,9 @@ public String getLocationsSampleFile(LocationWriter.LocationUploadMode locationU return sampleFileBuilder.toString(); } - public List getAddressLevelTypesForCreateModeSingleHierarchy(String locationHierarchy) throws Exception { + public List getAddressLevelTypesForCreateModeSingleHierarchy(String locationHierarchy) { if(!StringUtils.hasText(locationHierarchy)) { - throw new Exception(String.format("Invalid value specified for locationHierarchy: %s", locationHierarchy)); + throw new RuntimeException(String.format("Invalid value specified for locationHierarchy: %s", locationHierarchy)); } List selectedLocationHierarchy = Arrays.stream(locationHierarchy.split("\\.")) .map(Long::parseLong).collect(Collectors.toList()); @@ -388,13 +388,13 @@ private String getHeaderName(FormElement formElement) { return "\"" + conceptName + "\""; } - private String addToResponse(String inputString, List headers) { + private String addToResponse(String inputString, List headers) { String outputString = addCommaIfNecessary(inputString); return outputString.concat(String.join(",", headers)); } private String addCommaIfNecessary(String str) { - if (str.length() > 0) { + if (!str.isEmpty()) { return str.concat(","); } return str; diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterTest.java index 583b854c4..5b8a36eed 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterTest.java @@ -21,17 +21,14 @@ import static org.mockito.Mockito.*; public class LocationWriterTest { - private AddressLevelTypeRepository addressLevelTypeRepository; - private LocationRepository locationRepository; - private LocationService locationService; private ImportService importService; private LocationWriter locationWriter; @Before public void setup() { - addressLevelTypeRepository = mock(AddressLevelTypeRepository.class); - locationRepository = mock(LocationRepository.class); - locationService = mock(LocationService.class); + AddressLevelTypeRepository addressLevelTypeRepository = mock(AddressLevelTypeRepository.class); + LocationRepository locationRepository = mock(LocationRepository.class); + LocationService locationService = mock(LocationService.class); importService = mock(ImportService.class); locationWriter = new LocationWriter(locationService, locationRepository, addressLevelTypeRepository, mock(ObservationCreator.class), importService, mock(FormService.class)); when(locationService.save(any())).thenReturn(new AddressLevel()); From f3349d18d04ec5012b7185e727217aea9d078bf3 Mon Sep 17 00:00:00 2001 From: himeshr Date: Tue, 13 Aug 2024 16:04:51 +0530 Subject: [PATCH 078/129] avniproject/avni-webapp#787 | Fix for Multiple issues - Move gender to metadata - Dont allow deletion admin config without delMetaData - Move around idSource and idprefix - userSync settings - idempotent orgConfig, group and gender recreate - Clean up orgCongig settings during metadata delete - Fix issues with deletion of UserGroup - Fix Cascade of AddressLevel.virtualCatchments causing issue deleting addressLevels --- .../avni/server/dao/UserGroupRepository.java | 6 + .../org/avni/server/domain/AddressLevel.java | 4 +- .../java/org/avni/server/domain/User.java | 5 + .../avni/server/domain/VirtualCatchment.java | 2 +- .../service/OrganisationConfigService.java | 37 +- .../server/service/OrganisationService.java | 330 ++++++++++-------- .../org/avni/server/service/UserService.java | 6 +- .../server/web/ImplementationController.java | 27 +- .../server/web/OrganisationController.java | 2 +- ...SetNoOpTriggerForVirtCatchmentDeletion.sql | 14 + 10 files changed, 271 insertions(+), 162 deletions(-) create mode 100644 avni-server-api/src/main/resources/db/migration/V1_339_7__SetNoOpTriggerForVirtCatchmentDeletion.sql diff --git a/avni-server-api/src/main/java/org/avni/server/dao/UserGroupRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/UserGroupRepository.java index ef6c80b0c..d6c14d170 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/UserGroupRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/UserGroupRepository.java @@ -6,6 +6,8 @@ import org.avni.server.domain.UserGroup; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; import org.springframework.security.access.prepost.PreAuthorize; @@ -41,6 +43,10 @@ default UserGroup findByNameIgnoreCase(String name) { @RestResource(exported = false) Long deleteAllByGroupIsNotIn(List groups); + @Modifying + @Query("DELETE FROM UserGroup ug where ug.group in (:groups)") + int deleteAllByGroupIn(List groups); + boolean existsByUserIdAndLastModifiedDateTimeGreaterThan(Long userId, Date lastModifiedDateTime); List findByUserAndGroupHasAllPrivilegesTrueAndIsVoidedFalse(User user); diff --git a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java index 3d8a32291..3362619f3 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java @@ -52,11 +52,11 @@ public class AddressLevel extends OrganisationAwareEntity { @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "location") private Set parentLocationMappings = new HashSet<>(); - @ManyToMany(cascade = CascadeType.ALL) + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) @JoinTable(name = "catchment_address_mapping", joinColumns = {@JoinColumn(name = "addresslevel_id")}, inverseJoinColumns = {@JoinColumn(name = "catchment_id")}) private Set catchments = new HashSet<>(); - @ManyToMany() + @ManyToMany @Immutable @JoinTable(name = "virtual_catchment_address_mapping_table", joinColumns = {@JoinColumn(name = "addresslevel_id")}, inverseJoinColumns = {@JoinColumn(name = "catchment_id")}) private Set virtualCatchments = new HashSet<>(); diff --git a/avni-server-api/src/main/java/org/avni/server/domain/User.java b/avni-server-api/src/main/java/org/avni/server/domain/User.java index 80ca4fa94..0536d3342 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/User.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/User.java @@ -195,6 +195,11 @@ public void setCatchment(@NotNull Catchment catchment) { this.catchment = catchment; } + public void removeCatchment() { + this.catchment = null; + this.operatingIndividualScope = OperatingIndividualScope.None; + } + public Boolean isVoided() { return isVoided; } diff --git a/avni-server-api/src/main/java/org/avni/server/domain/VirtualCatchment.java b/avni-server-api/src/main/java/org/avni/server/domain/VirtualCatchment.java index 9eda777ea..3d02835de 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/VirtualCatchment.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/VirtualCatchment.java @@ -23,7 +23,7 @@ public class VirtualCatchment { @JoinColumn(name = "catchment_id") private Catchment catchment; - @ManyToOne(cascade = {CascadeType.ALL}) + @ManyToOne @JoinColumn(name = "addresslevel_id") private AddressLevel addressLevel; diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationConfigService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationConfigService.java index 0d4c31992..3846af61a 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationConfigService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationConfigService.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import org.avni.server.application.FormMapping; import org.avni.server.application.KeyType; import org.avni.server.application.OrganisationConfigSettingKey; import org.avni.server.dao.ConceptRepository; @@ -80,13 +79,18 @@ public OrganisationConfig saveOrganisationConfig(OrganisationConfigRequest reque } public OrganisationConfig createDefaultOrganisationConfig(Organisation organisation) { - OrganisationConfig organisationConfig = new OrganisationConfig(); - organisationConfig.assignUUID(); + OrganisationConfig organisationConfig = organisationConfigRepository.findByOrganisationId(organisation.getId()); + if (Objects.isNull(organisationConfig)) { + organisationConfig = new OrganisationConfig(); + organisationConfig.assignUUID(); + organisationConfig.setOrganisationId(organisation.getId()); + } Map settings = new HashMap<>(); settings.put("languages", new String[]{"en"}); JsonObject jsonObject = new JsonObject(settings); organisationConfig.setSettings(jsonObject); - organisationConfig.setOrganisationId(organisation.getId()); + organisationConfig.setExportSettings(new JsonObject()); + organisationConfig.setWorklistUpdationRule(null); return organisationConfigRepository.save(organisationConfig); } @@ -324,4 +328,29 @@ public ResponseEntity deleteExportSettings(String name) { organisationConfigRepository.save(organisationConfig); return ResponseEntity.ok().build(); } + + public OrganisationConfig deleteMetadataRelatedSettings() { + OrganisationConfig organisationConfig = getCurrentOrganisationConfig(); + organisationConfig.setSettings(updateOrganisationConfigSettings(getMetadataConfigSettingsAfterReset(), + organisationConfig.getSettings())); + organisationConfig.setExportSettings(new JsonObject()); + organisationConfig.setWorklistUpdationRule(null); + organisationConfig.updateAudit(); + return organisationConfigRepository.save(organisationConfig); + } + + private JsonObject getMetadataConfigSettingsAfterReset() { + OrganisationConfigSettingKey[] metadataRelatedConfigSettingKeys = { + OrganisationConfigSettingKey.searchFilters, + OrganisationConfigSettingKey.customRegistrationLocations, + OrganisationConfigSettingKey.myDashboardFilters, + OrganisationConfigSettingKey.searchResultFields, + OrganisationConfigSettingKey.lowestAddressLevelType + }; + JsonObject settings = new JsonObject(); + Arrays.asList(metadataRelatedConfigSettingKeys).stream() + .forEach(settingKey -> settings.put(String.valueOf(settingKey), Collections.emptyList())); + settings.put(OrganisationConfig.Extension.EXTENSION_DIR, Collections.emptyList()); + return settings; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index 1319dbf52..72746f580 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -353,6 +353,147 @@ public OrganisationService(FormRepository formRepository, logger = LoggerFactory.getLogger(this.getClass()); } + /* Tables that are not part of any org data clean up are as follows: + * organisationCategoryRepository, // Not org specific + * organisationStatusRepository, // Not org specific + * organisationGroupOrganisationRepository, // Deleting orgGroup Org is out of scope + * organisationGroupRepository, // Deleting orgGroup is out of scope + * organisationRepository, // Deleting org itself is out of scope + * + * batch_job_instance // Not org specific + * batch_job_execution // Not org specific + * batch_step_execution // Not org specific + * batch_job_execution_context // Not org specific + * batch_job_execution_params // Not org specific + * batch_step_execution_context // Not org specific + * + * audit //Deprecated + * facility //Deprecated + * program_outcome //Deprecated + * dashboard_card_mapping //Deprecated + * deps_saved_ddl // Not org specific + * schema_version // Not org specific + * scheduled_job_run // Not org specific + * privilegeRepository, // Not org specific + * flyway_schema_history // Not org specific + * approvalStatusRepository, // Not org specific + * platformTranslationRepository, // Not org specific + * standardReportCardTypeRepository, // Not org specific + */ + + private CrudRepository[] getTxCrudRepositories() { + CrudRepository[] txCrudRepositories = { + exportJobParametersRepository, + manualMessageRepository, + messageReceiverRepository, + messageRequestQueueRepository, + ruleFailureLogRepository, + }; + return txCrudRepositories; + } + + private JpaRepository[] getTxJpaRepositories() { + JpaRepository[] transactionalRepositories = { + newsRepository, + commentRepository, + commentThreadRepository, + entityApprovalStatusRepository, + ruleFailureTelemetryRepository, + identifierAssignmentRepository, + syncTelemetryRepository, + videoTelemetricRepository, + groupSubjectRepository, + individualRelationshipRepository, + checklistItemRepository, + checklistRepository, + programEncounterRepository, + programEnrolmentRepository, + encounterRepository, + subjectMigrationRepository, + userSubjectAssignmentRepository, + subjectProgramEligibilityRepository, + taskUnAssignmentRepository, + taskRepository, + userSubjectRepository, + individualRepository, + resetSyncRepository, + }; + return transactionalRepositories; + } + + private CrudRepository[] getMetadataCrudRepositories() { + CrudRepository[] metadataCrudRepositories = { + messageRuleRepository, + customQueryRepository, + }; + return metadataCrudRepositories; + } + + private JpaRepository[] getMetadataJpaRepositories() { + JpaRepository[] metadataRepositories = { + groupPrivilegeRepository, + groupRoleRepository, + checklistItemDetailRepository, + checklistDetailRepository, + individualRelationGenderMappingRepository, + individualRelationshipTypeRepository, + individualRelationRepository, + formElementRepository, + formElementGroupRepository, + formMappingRepository, + formRepository, + conceptAnswerRepository, + conceptRepository, + operationalEncounterTypeRepository, + encounterTypeRepository, + operationalProgramRepository, + programRepository, + operationalSubjectTypeRepository, + subjectTypeRepository, + translationRepository, + videoRepository, + dashboardSectionCardMappingRepository, + cardRepository, + dashboardSectionRepository, + groupDashboardRepository, + dashboardRepository, + answerConceptMigrationRepository, + dashboardFilterRepository, + documentationItemRepository, + documentationRepository, + menuItemRepository, + ruleRepository, + ruleDependencyRepository, + taskTypeRepository, + taskStatusRepository, + translationRepository, + userSubjectAssignmentRepository, + }; + return metadataRepositories; + } + + private CrudRepository[] getAdminConfigCrudRepositories() { + CrudRepository[] adminConfigCrudRepositories = { + externalSystemConfigRepository, + }; + return adminConfigCrudRepositories; + } + + private JpaRepository[] getAdminConfigJPARepositories() { + JpaRepository[] adminConfigRepositories = { + msg91ConfigRepository, + identifierUserAssignmentRepository, + identifierSourceRepository, + genderRepository, + catchmentRepository, + locationMappingRepository, + locationRepository, + addressLevelTypeRepository, + organisationConfigRepository, + }; + return adminConfigRepositories; + } + public void addOrganisationConfig(Long orgId, ZipOutputStream zos) throws IOException { OrganisationConfig organisationConfig = organisationConfigRepository.findByOrganisationId(orgId); if (organisationConfig != null) { @@ -595,7 +736,7 @@ public void addSubjectTypeIcons(ZipOutputStream zos) throws IOException { public void addReportCardIcons(ZipOutputStream zos) throws IOException { List cards = cardRepository.findAllByIconFileS3KeyNotNull().stream() - .filter(card -> !card.getIconFileS3Key().trim().isEmpty()).collect(Collectors.toList()); + .filter(card -> !card.getIconFileS3Key().trim().isEmpty()).collect(Collectors.toList()); if (cards.size() > 0) { addDirectoryToZip(zos, BundleFolder.REPORT_CARD_ICONS.getFolderName()); } @@ -645,6 +786,7 @@ public void addTaskStatus(ZipOutputStream zos) throws IOException { public void addApplicationMenus(ZipOutputStream zos) throws IOException { addFileToZip(zos, "menuItem.json", menuItemService.findAll().stream().map(MenuItemContract::new).collect(Collectors.toList())); } + public void addMessageRules(ZipOutputStream zos) throws IOException { addFileToZip(zos, "messageRule.json", messagingService.findAll().stream().map(messageRule -> new MessageRuleContract(messageRule, entityTypeRetrieverService)).collect(Collectors.toList())); } @@ -713,110 +855,23 @@ private void addDirectoryToZip(ZipOutputStream zos, String directoryName) throws zos.closeEntry(); } - public void deleteTransactionalData(Organisation organisation) { deleteNonRepositoryTransactionalData(organisation); - JpaRepository[] transactionalRepositories = { - newsRepository, - commentRepository, - commentThreadRepository, - entityApprovalStatusRepository, - ruleFailureTelemetryRepository, - identifierAssignmentRepository, - syncTelemetryRepository, - videoTelemetricRepository, - groupSubjectRepository, - individualRelationshipRepository, - checklistItemRepository, - checklistRepository, - programEncounterRepository, - programEnrolmentRepository, - encounterRepository, - subjectMigrationRepository, - userSubjectAssignmentRepository, - subjectProgramEligibilityRepository, - taskUnAssignmentRepository, - taskRepository, - userSubjectRepository, - individualRepository, - resetSyncRepository, - }; - - CrudRepository[] txCrudRepositories = { - exportJobParametersRepository, - manualMessageRepository, - messageReceiverRepository, - messageRequestQueueRepository, - ruleFailureLogRepository, - }; - - Arrays.asList(txCrudRepositories).forEach(this::deleteAll); - Arrays.asList(transactionalRepositories).forEach(this::deleteAll); + Arrays.asList(getTxCrudRepositories()).forEach(this::deleteAll); + Arrays.asList(getTxJpaRepositories()).forEach(this::deleteAll); } public void deleteMetadata(Organisation organisation) { deleteNonRepositoryMetadata(organisation); - JpaRepository[] metadataRepositories = { - groupPrivilegeRepository, - groupRoleRepository, - checklistItemDetailRepository, - checklistDetailRepository, - identifierUserAssignmentRepository, - identifierSourceRepository, - individualRelationGenderMappingRepository, - individualRelationRepository, - individualRelationshipTypeRepository, - formElementRepository, - formElementGroupRepository, - formMappingRepository, - formRepository, - conceptAnswerRepository, - conceptRepository, - operationalEncounterTypeRepository, - encounterTypeRepository, - operationalProgramRepository, - programRepository, - operationalSubjectTypeRepository, - subjectTypeRepository, - organisationConfigRepository, - translationRepository, - videoRepository, - dashboardSectionCardMappingRepository, - cardRepository, - dashboardSectionRepository, - groupDashboardRepository, - dashboardRepository, - msg91ConfigRepository, - genderRepository, - userGroupRepository, - groupRepository, - answerConceptMigrationRepository, - dashboardFilterRepository, - documentationRepository, - documentationItemRepository, - menuItemRepository, - ruleRepository, - ruleDependencyRepository, - taskTypeRepository, - taskStatusRepository, - translationRepository, - userSubjectAssignmentRepository, - }; - - CrudRepository[] metadataCrudRepositories = { - messageRuleRepository, - customQueryRepository, - }; - - Arrays.asList(metadataRepositories).forEach(this::deleteAll); - Arrays.asList(metadataCrudRepositories).forEach(this::deleteAll); - + Arrays.asList(getMetadataJpaRepositories()).forEach(this::deleteAll); + Arrays.asList(getMetadataCrudRepositories()).forEach(this::deleteAll); + userRepository.findAllByOrganisationId(organisation.getId()).stream().forEach(user -> user.setSyncSettings(new JsonObject())); } -public void deleteNonRepositoryTransactionalData(Organisation organisation) { - String individualRelativeDeletionQuery = "delete from individual_relative where organisation_id = %d and organisation_id > 1"; - jdbcTemplate.execute(String.format(individualRelativeDeletionQuery, organisation.getId())); -} + public void deleteNonRepositoryTransactionalData(Organisation organisation) { + String individualRelativeDeletionQuery = "delete from individual_relative where organisation_id = %d and organisation_id > 1"; + jdbcTemplate.execute(String.format(individualRelativeDeletionQuery, organisation.getId())); + } public void deleteNonRepositoryMetadata(Organisation organisation) { String decisionConceptsDeletionQuery = "delete from decision_concept dc using concept c where dc.concept_id = c.id and c.organisation_id = %d and c.organisation_id > 1"; @@ -826,61 +881,40 @@ public void deleteNonRepositoryMetadata(Organisation organisation) { } public void deleteAdminConfigData(Organisation organisation) { + removeCatchmentAssignmentAndDeleteNonAdminUsers(organisation); + deleteNonDefaultGroupsAndTheirMappings(); deleteNonRepositoryAdminConfigData(organisation); - JpaRepository[] adminConfigRepositories = { - catchmentRepository, - locationMappingRepository, - locationRepository, - addressLevelTypeRepository, - msg91ConfigRepository, - -// organisationCategoryRepository, // Not org specific -// organisationStatusRepository, // Not org specific -// organisationGroupOrganisationRepository, // Deleting orgGroup Org is out of scope -// organisationGroupRepository, // Deleting orgGroup is out of scope -// organisationRepository, // Deleting org itself is out of scope - -// batch_job_instance // Not org specific -// batch_job_execution // Not org specific -// batch_step_execution // Not org specific -// batch_job_execution_context // Not org specific -// batch_job_execution_params // Not org specific -// batch_step_execution_context // Not org specific - -// audit //Deprecated -// facility //Deprecated -// program_outcome //Deprecated -// dashboard_card_mapping //Deprecated -// deps_saved_ddl // Not org specific -// schema_version // Not org specific -// scheduled_job_run // Not org specific -// privilegeRepository, // Not org specific -// flyway_schema_history // Not org specific -// approvalStatusRepository, // Not org specific -// platformTranslationRepository, // Not org specific -// standardReportCardTypeRepository, // Not org specific - }; + Arrays.asList(getAdminConfigCrudRepositories()).forEach(this::deleteAll); + Arrays.asList(getAdminConfigJPARepositories()).forEach(this::deleteAll); + } - CrudRepository[] adminConfigCrudRepositories = { - externalSystemConfigRepository, - }; + private void deleteNonDefaultGroupsAndTheirMappings() { + List nonDefaultGroups = groupRepository.findAll().stream() + .filter(group -> !group.isOneOfTheDefaultGroups()) + .collect(Collectors.toList()); + userGroupRepository.deleteAllByGroupIn(nonDefaultGroups); + groupRepository.deleteAll(nonDefaultGroups); + } - Arrays.asList(adminConfigRepositories).forEach(this::deleteAll); - Arrays.asList(adminConfigCrudRepositories).forEach(this::deleteAll); - userRepository.findAllByOrganisationId(organisation.getId()).stream().filter(user -> !user.hasAllPrivileges()).forEach(user -> userService.deleteUser(user.getId())); + private void removeCatchmentAssignmentAndDeleteNonAdminUsers(Organisation organisation) { + userRepository.findAllByOrganisationId(organisation.getId()).stream().forEach(user -> user.removeCatchment()); + userRepository.findAllByOrganisationId(organisation.getId()).stream() + .filter(user -> !user.hasAllPrivileges()).forEach(user -> userService.deleteUser(user.getId())); } public void deleteNonRepositoryAdminConfigData(Organisation organisation) { - String catchmentAddressMappingDeletionQuery = "delete from catchment_address_mapping cam using address_level al where cam.addresslevel_id = al.id and al.organisation_id = %d and al.organisation_id > 1)"; - jdbcTemplate.execute(String.format(catchmentAddressMappingDeletionQuery, organisation.getId().toString())); + String catchmentAddressMappingDeletionQuery = "delete from catchment_address_mapping cam using address_level al where cam.addresslevel_id = al.id and al.organisation_id = %d and al.organisation_id > 1"; + jdbcTemplate.execute(String.format(catchmentAddressMappingDeletionQuery, organisation.getId())); } private void deleteAll(JpaRepository repository) { repository.deleteAllInBatch(); } + private void deleteAll(CrudRepository repository) { repository.deleteAll(); } + public void deleteMediaContent(boolean deleteMetadata) { try { s3Service.deleteOrgMedia(deleteMetadata); @@ -888,6 +922,7 @@ public void deleteMediaContent(boolean deleteMetadata) { logger.info("Error while deleting the media files, skipping."); } } + public void deleteETLData(Organisation organisation) { String baseQuery = "select delete_etl_metadata_for_schema('$impl_schema', '$impl_db_user', '$impl_db_owner')"; String query = baseQuery @@ -906,6 +941,9 @@ public void addGroupDashboardJson(ZipOutputStream zos) throws IOException { } private void createGender(String genderName, Organisation org) { + if (Objects.nonNull(genderRepository.findByName(genderName))) { + return; + } Gender gender = new Gender(); gender.setName(genderName); gender.assignUUID(); @@ -913,7 +951,10 @@ private void createGender(String genderName, Organisation org) { genderRepository.save(gender); } - private void addDefaultGroup(Long organisationId, String groupType) { + private void addDefaultGroupIfNotPresent(Long organisationId, String groupType) { + if (Objects.nonNull(groupRepository.findByNameAndOrganisationId(groupType, organisationId))) { + return; + } Group group = new Group(); group.setName(groupType); group.setOrganisationId(organisationId); @@ -929,13 +970,16 @@ private void createDefaultGenders(Organisation org) { createGender("Other", org); } - public void setupBaseOrganisationData(Organisation organisation) { - createDefaultGenders(organisation); - addDefaultGroup(organisation.getId(), Group.Everyone); - addDefaultGroup(organisation.getId(), Group.Administrators); + public void setupBaseOrganisationAdminConfig(Organisation organisation) { organisationConfigService.createDefaultOrganisationConfig(organisation); } + public void setupBaseOrganisationMetadata(Organisation organisation) { + createDefaultGenders(organisation); + addDefaultGroupIfNotPresent(organisation.getId(), Group.Everyone); + addDefaultGroupIfNotPresent(organisation.getId(), Group.Administrators); + } + public Organisation getCurrentOrganisation() { Long organisationId = UserContextHolder.getUserContext().getOrganisationId(); return organisationRepository.findOne(organisationId); diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserService.java b/avni-server-api/src/main/java/org/avni/server/service/UserService.java index 320c5090f..673e1689d 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserService.java @@ -19,10 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import static org.avni.messaging.domain.Constants.NO_OF_DIGITS_IN_INDIAN_MOBILE_NO; @@ -200,6 +197,7 @@ public ResponseEntity deleteUser(Long id) { idpServiceFactory.getIdpService(user).deleteUser(user); user.setVoided(true); user.setDisabledInCognito(true); + user.setUserGroups(user.getUserGroups().stream().filter(ug -> ug.getGroup().isEveryone()).collect(Collectors.toList())); userRepository.save(user); logger.info(String.format("Deleted user '%s', UUID '%s'", user.getUsername(), user.getUuid())); return new ResponseEntity<>(user, HttpStatus.CREATED); diff --git a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java index 5bbbc24a3..108862908 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java @@ -5,6 +5,7 @@ import org.avni.server.domain.accessControl.PrivilegeType; import org.avni.server.domain.organisation.OrganisationCategory; import org.avni.server.framework.security.UserContextHolder; +import org.avni.server.service.OrganisationConfigService; import org.avni.server.service.OrganisationService; import org.avni.server.service.accessControl.AccessControlService; import org.springframework.beans.factory.annotation.Autowired; @@ -27,11 +28,13 @@ public class ImplementationController implements RestControllerResourceProcessor { private final OrganisationService organisationService; + private final OrganisationConfigService organisationConfigService; private final AccessControlService accessControlService; @Autowired - public ImplementationController(OrganisationService organisationService, AccessControlService accessControlService) { + public ImplementationController(OrganisationService organisationService, OrganisationConfigService organisationConfigService, AccessControlService accessControlService) { this.organisationService = organisationService; + this.organisationConfigService = organisationConfigService; this.accessControlService = accessControlService; } @@ -111,11 +114,13 @@ public ResponseEntity delete(@Param("deleteMetadata") boolean deleteMetadata, if (OrganisationCategory.Production.equals(organisation.getCategory().getName())) { return new ResponseEntity<>("Production organisation's data cannot be deleted", HttpStatus.CONFLICT); } - + //Delete checkPrivilegeAndDeleteTransactionalData(organisation); - checkPrivilegeAndDeleteMetadata(deleteMetadata, organisation); selectivelyCleanupMediaContent(deleteMetadata); - checkPrivilegeAndDeleteAdminConfigurationData(deleteAdminConfig, organisation); + checkPrivilegeAndDeleteMetadata(deleteMetadata, organisation); + checkPrivilegeAndDeleteAdminConfig(deleteAdminConfig, organisation); + //Recreate + checkPrivilegeAndRecreateBasicAdminConfig(deleteAdminConfig); checkPrivilegeAndRecreateBasicMetadata(deleteMetadata); return new ResponseEntity<>(HttpStatus.OK); @@ -124,11 +129,18 @@ public ResponseEntity delete(@Param("deleteMetadata") boolean deleteMetadata, private void checkPrivilegeAndRecreateBasicMetadata(boolean deleteMetadata) { if (deleteMetadata) { accessControlService.checkPrivilege(PrivilegeType.UploadMetadataAndData); - organisationService.setupBaseOrganisationData(UserContextHolder.getOrganisation()); + organisationService.setupBaseOrganisationMetadata(UserContextHolder.getOrganisation()); } } - private void checkPrivilegeAndDeleteAdminConfigurationData(boolean deleteAdminConfig, Organisation organisation) { + private void checkPrivilegeAndRecreateBasicAdminConfig(boolean deleteAdminConfig) { + if (deleteAdminConfig) { + accessControlService.checkPrivilege(PrivilegeType.DeleteOrganisationConfiguration); + organisationService.setupBaseOrganisationAdminConfig(UserContextHolder.getOrganisation()); + } + } + + private void checkPrivilegeAndDeleteAdminConfig(boolean deleteAdminConfig, Organisation organisation) { if(deleteAdminConfig){ accessControlService.checkPrivilege(PrivilegeType.DeleteOrganisationConfiguration); organisationService.deleteAdminConfigData(organisation); @@ -147,8 +159,9 @@ private void selectivelyCleanupMediaContent(boolean deleteMetadata) { private void checkPrivilegeAndDeleteMetadata(boolean deleteMetadata, Organisation organisation) { if (deleteMetadata) { accessControlService.checkPrivilege(PrivilegeType.UploadMetadataAndData); - organisationService.deleteMetadata(organisation); organisationService.deleteETLData(organisation); + organisationConfigService.deleteMetadataRelatedSettings(); + organisationService.deleteMetadata(organisation); } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java index c6fc8a43e..bd68c0ed3 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java @@ -60,7 +60,7 @@ public ResponseEntity save(@RequestBody OrganisationContract request) { setOrgAccountByIdOrDefault(org, request.getAccountId()); organisationRepository.save(org); - organisationService.setupBaseOrganisationData(org); + organisationService.setupBaseOrganisationAdminConfig(org); return new ResponseEntity<>(org, HttpStatus.CREATED); } diff --git a/avni-server-api/src/main/resources/db/migration/V1_339_7__SetNoOpTriggerForVirtCatchmentDeletion.sql b/avni-server-api/src/main/resources/db/migration/V1_339_7__SetNoOpTriggerForVirtCatchmentDeletion.sql new file mode 100644 index 000000000..260f34c0a --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_339_7__SetNoOpTriggerForVirtCatchmentDeletion.sql @@ -0,0 +1,14 @@ +CREATE OR REPLACE FUNCTION no_op() RETURNS trigger AS $$ +BEGIN + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER delete_on_virtual_catchment_address_mapping + INSTEAD OF DELETE + ON virtual_catchment_address_mapping_table + FOR EACH ROW +EXECUTE FUNCTION no_op(); + +SELECT grant_all_on_table(a.rolname, 'virtual_catchment_address_mapping_table') FROM pg_roles a WHERE pg_has_role('openchs', a.oid, 'member'); + From 5d3dd5747d7f24899e450fe56c4c9ff1a1116e9f Mon Sep 17 00:00:00 2001 From: Joy A Date: Tue, 13 Aug 2024 16:46:21 +0530 Subject: [PATCH 079/129] #770 | Bundle - Std Report Card - Set recent duration input only if recent card --- .../src/main/java/org/avni/server/service/CardService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/CardService.java b/avni-server-api/src/main/java/org/avni/server/service/CardService.java index ecd92efb5..9008bc7eb 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CardService.java @@ -113,8 +113,8 @@ private void buildStandardReportCardInputs(StandardReportCardType type, List Date: Tue, 13 Aug 2024 17:31:59 +0530 Subject: [PATCH 080/129] avniproject/avni-webapp#787 | Functions and Views should always be part of Repeatable migration --- Makefile | 3 +++ .../main/resources/db/migration/R__Functions.sql | 6 ++++++ .../src/main/resources/db/migration/R__Views.sql | 7 +++++++ ...9_7__SetNoOpTriggerForVirtCatchmentDeletion.sql | 14 -------------- 4 files changed, 16 insertions(+), 14 deletions(-) delete mode 100644 avni-server-api/src/main/resources/db/migration/V1_339_7__SetNoOpTriggerForVirtCatchmentDeletion.sql diff --git a/Makefile b/Makefile index 0ec7758a4..286e916ff 100644 --- a/Makefile +++ b/Makefile @@ -140,6 +140,9 @@ boot_run: test_server: rebuild_testdb ## Run tests GRADLE_OPTS="-Xmx256m" ./gradlew clean test +test_server_without_clean_rebuild: ## Run tests + GRADLE_OPTS="-Xmx256m" ./gradlew test + test_server_with_remote_db: make rebuild_testdb su=$(DBUSER) dbServer=$(DBSERVER) OPENCHS_DATABASE_URL=jdbc:postgresql://$(DBSERVER):5432/openchs_test GRADLE_OPTS="-Xmx256m" ./gradlew clean test diff --git a/avni-server-api/src/main/resources/db/migration/R__Functions.sql b/avni-server-api/src/main/resources/db/migration/R__Functions.sql index 5c9dc3023..c5dc431dc 100644 --- a/avni-server-api/src/main/resources/db/migration/R__Functions.sql +++ b/avni-server-api/src/main/resources/db/migration/R__Functions.sql @@ -323,3 +323,9 @@ BEGIN END; END $$ STABLE; + +CREATE OR REPLACE FUNCTION no_op() RETURNS trigger AS $$ +BEGIN + RETURN NULL; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/avni-server-api/src/main/resources/db/migration/R__Views.sql b/avni-server-api/src/main/resources/db/migration/R__Views.sql index ae704c311..119bd8bb8 100644 --- a/avni-server-api/src/main/resources/db/migration/R__Views.sql +++ b/avni-server-api/src/main/resources/db/migration/R__Views.sql @@ -31,6 +31,13 @@ CREATE or replace VIEW virtual_catchment_address_mapping_table AS SELECT * FROM virtual_catchment_address_mapping_table_function(); +DROP TRIGGER IF EXISTS delete_on_virtual_catchment_address_mapping ON virtual_catchment_address_mapping_table CASCADE; + +CREATE TRIGGER delete_on_virtual_catchment_address_mapping + INSTEAD OF DELETE + ON virtual_catchment_address_mapping_table + FOR EACH ROW +EXECUTE FUNCTION no_op(); DROP VIEW if exists address_level_type_view; diff --git a/avni-server-api/src/main/resources/db/migration/V1_339_7__SetNoOpTriggerForVirtCatchmentDeletion.sql b/avni-server-api/src/main/resources/db/migration/V1_339_7__SetNoOpTriggerForVirtCatchmentDeletion.sql deleted file mode 100644 index 260f34c0a..000000000 --- a/avni-server-api/src/main/resources/db/migration/V1_339_7__SetNoOpTriggerForVirtCatchmentDeletion.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE OR REPLACE FUNCTION no_op() RETURNS trigger AS $$ -BEGIN - RETURN NULL; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER delete_on_virtual_catchment_address_mapping - INSTEAD OF DELETE - ON virtual_catchment_address_mapping_table - FOR EACH ROW -EXECUTE FUNCTION no_op(); - -SELECT grant_all_on_table(a.rolname, 'virtual_catchment_address_mapping_table') FROM pg_roles a WHERE pg_has_role('openchs', a.oid, 'member'); - From 77cd80f702f1a0f75ebafc897f4e23b703be753b Mon Sep 17 00:00:00 2001 From: himeshr Date: Tue, 13 Aug 2024 19:10:23 +0530 Subject: [PATCH 081/129] avniproject/avni-webapp#787 | Fix minor issues with ordering and checks --- .../java/org/avni/server/service/OrganisationService.java | 8 +++++--- .../main/java/org/avni/server/service/StorageService.java | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index 72746f580..41784f04b 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -456,9 +456,9 @@ private JpaRepository[] getMetadataJpaRepositories() { cardRepository, dashboardSectionRepository, groupDashboardRepository, + dashboardFilterRepository, dashboardRepository, answerConceptMigrationRepository, - dashboardFilterRepository, documentationItemRepository, documentationRepository, menuItemRepository, @@ -892,8 +892,10 @@ private void deleteNonDefaultGroupsAndTheirMappings() { List nonDefaultGroups = groupRepository.findAll().stream() .filter(group -> !group.isOneOfTheDefaultGroups()) .collect(Collectors.toList()); - userGroupRepository.deleteAllByGroupIn(nonDefaultGroups); - groupRepository.deleteAll(nonDefaultGroups); + if(nonDefaultGroups != null && !nonDefaultGroups.isEmpty()) { + userGroupRepository.deleteAllByGroupIn(nonDefaultGroups); + groupRepository.deleteAll(nonDefaultGroups); + } } private void removeCatchmentAssignmentAndDeleteNonAdminUsers(Organisation organisation) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/StorageService.java b/avni-server-api/src/main/java/org/avni/server/service/StorageService.java index 666fd3e8b..0acc845e0 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/StorageService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/StorageService.java @@ -13,7 +13,6 @@ import org.avni.server.domain.Organisation; import org.avni.server.domain.OrganisationConfig; import org.avni.server.domain.UserContext; -import org.avni.server.domain.accessControl.PrivilegeType; import org.avni.server.framework.security.UserContextHolder; import org.avni.server.util.AvniFiles; import org.avni.server.util.S; From 6ab9eb93dca9de5132cdeef103f07be88fcea7c5 Mon Sep 17 00:00:00 2001 From: himeshr Date: Tue, 13 Aug 2024 19:13:21 +0530 Subject: [PATCH 082/129] avniproject/avni-webapp#787 | Fix minor issues with ordering and checks --- .../server/service/OrganisationService.java | 28 ------------------- .../server/web/ImplementationController.java | 2 +- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index 41784f04b..e0e1eb49b 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -353,34 +353,6 @@ public OrganisationService(FormRepository formRepository, logger = LoggerFactory.getLogger(this.getClass()); } - /* Tables that are not part of any org data clean up are as follows: - * organisationCategoryRepository, // Not org specific - * organisationStatusRepository, // Not org specific - * organisationGroupOrganisationRepository, // Deleting orgGroup Org is out of scope - * organisationGroupRepository, // Deleting orgGroup is out of scope - * organisationRepository, // Deleting org itself is out of scope - * - * batch_job_instance // Not org specific - * batch_job_execution // Not org specific - * batch_step_execution // Not org specific - * batch_job_execution_context // Not org specific - * batch_job_execution_params // Not org specific - * batch_step_execution_context // Not org specific - * - * audit //Deprecated - * facility //Deprecated - * program_outcome //Deprecated - * dashboard_card_mapping //Deprecated - * deps_saved_ddl // Not org specific - * schema_version // Not org specific - * scheduled_job_run // Not org specific - * privilegeRepository, // Not org specific - * flyway_schema_history // Not org specific - * approvalStatusRepository, // Not org specific - * platformTranslationRepository, // Not org specific - * standardReportCardTypeRepository, // Not org specific - */ - private CrudRepository[] getTxCrudRepositories() { CrudRepository[] txCrudRepositories = { exportJobParametersRepository, diff --git a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java index 108862908..b7da6e33f 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java @@ -122,7 +122,7 @@ public ResponseEntity delete(@Param("deleteMetadata") boolean deleteMetadata, //Recreate checkPrivilegeAndRecreateBasicAdminConfig(deleteAdminConfig); checkPrivilegeAndRecreateBasicMetadata(deleteMetadata); - + //Refer to OrganisationService git history for list of repos and tables excluded from deletion flow due to valid causes return new ResponseEntity<>(HttpStatus.OK); } From df439334e4bbea455f8573ee44ec09d735f0f826 Mon Sep 17 00:00:00 2001 From: Joy A Date: Wed, 14 Aug 2024 10:17:08 +0530 Subject: [PATCH 083/129] #770 | Use privilege type instead of uuid as key for group privilege bundle uploads privilege uuid will be different for different installations --- .../batch/zip/BundleZipFileImporter.java | 4 +- .../server/service/OrganisationService.java | 4 +- .../accessControl/GroupPrivilegeService.java | 45 ++++++++++--------- .../request/GroupPrivilegeBundleContract.java | 34 ++++++++++++++ .../request/GroupPrivilegeContractWeb.java | 18 -------- 5 files changed, 61 insertions(+), 44 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeBundleContract.java diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java index de251fd93..85380d555 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java @@ -332,8 +332,8 @@ private void deployFile(String fileName, String fileData, List groupPrivileges = groupPrivilegeRepository.findAll().stream() + List groupPrivileges = groupPrivilegeRepository.findAll().stream() .filter(groupPrivilege -> !groupPrivilege.getGroup().isAdministrator()) .filter(groupPrivilege -> groupPrivilege.getImplVersion() == GroupPrivilege.IMPL_VERSION) - .map(GroupPrivilegeContractWeb::fromEntity).collect(Collectors.toList()); + .map(GroupPrivilegeBundleContract::fromEntity).collect(Collectors.toList()); if (!groupPrivileges.isEmpty()) { addFileToZip(zos, "groupPrivilege.json", groupPrivileges); } diff --git a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java index 4488a55f4..b1846ab63 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/accessControl/GroupPrivilegeService.java @@ -15,14 +15,14 @@ import org.avni.server.framework.security.UserContextHolder; import org.avni.server.service.NonScopeAwareService; import org.avni.server.util.CollectionUtil; +import org.avni.server.web.request.GroupPrivilegeBundleContract; import org.avni.server.web.request.GroupPrivilegeContractWeb; import org.joda.time.DateTime; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; @Service public class GroupPrivilegeService implements NonScopeAwareService { @@ -175,44 +175,45 @@ public List getAllGroupPrivileges(long groupId) { return allGroupPrivileges; } - public void savePrivileges(GroupPrivilegeContractWeb[] requests, Organisation organisation) { + public void savePrivilegesFromBundle(GroupPrivilegeBundleContract[] groupPrivilegeBundleContracts, Organisation organisation) { List groupPrivileges = groupPrivilegeRepository.findByImplVersion(GroupPrivilege.IMPL_VERSION); - List privileges = privilegeRepository.findAll(); + Map privilegeMapByType = privilegeRepository.findAll().stream().collect(Collectors.toMap(Privilege::getType, Function.identity())); List subjectTypes = subjectTypeRepository.findAll(); List programs = programRepository.findAll(); List encounterTypes = encounterTypeRepository.findAll(); List checklistDetails = checklistDetailRepository.findAll(); List groups = groupRepository.findAll(); - Arrays.stream(requests).forEach(request -> { + Arrays.stream(groupPrivilegeBundleContracts).forEach(groupPrivilegeBundleContract -> { try { - Group targetedGroup = getGroup(request, organisation, groups); + Group targetedGroup = getGroup(groupPrivilegeBundleContract, organisation, groups); GroupPrivilege groupPrivilege = groupPrivileges.stream().filter(gp -> Objects.equals(targetedGroup.getUuid(), gp.getGroupUuid()) - && Objects.equals(request.getPrivilegeUUID(), gp.getPrivilegeUuid()) - && Objects.equals(request.getSubjectTypeUUID(), gp.getSubjectTypeUuid()) - && Objects.equals(request.getProgramUUID(), gp.getProgramUuid()) - && Objects.equals(request.getProgramEncounterTypeUUID(), gp.getProgramEncounterTypeUuid()) - && Objects.equals(request.getEncounterTypeUUID(), gp.getEncounterTypeUuid()) - && Objects.equals(request.getChecklistDetailUUID(), gp.getChecklistDetailUuid())) + // rely on type since privilege uuid could be different across different installations + && Objects.equals(groupPrivilegeBundleContract.getPrivilegeType(), gp.getPrivilege().getType()) + && Objects.equals(groupPrivilegeBundleContract.getSubjectTypeUUID(), gp.getSubjectTypeUuid()) + && Objects.equals(groupPrivilegeBundleContract.getProgramUUID(), gp.getProgramUuid()) + && Objects.equals(groupPrivilegeBundleContract.getProgramEncounterTypeUUID(), gp.getProgramEncounterTypeUuid()) + && Objects.equals(groupPrivilegeBundleContract.getEncounterTypeUUID(), gp.getEncounterTypeUuid()) + && Objects.equals(groupPrivilegeBundleContract.getChecklistDetailUUID(), gp.getChecklistDetailUuid())) .findAny().orElse(null); if (groupPrivilege == null) { groupPrivilege = new GroupPrivilege(); //don't use uuid from request for bundle uploads since there could be records with matching uuid with older impl_version in db and unique org_uuid constraint is violated groupPrivilege.assignUUID(); - groupPrivilege.setPrivilege(CollectionUtil.findByUuid(privileges, request.getPrivilegeUUID())); - groupPrivilege.setSubjectType(CollectionUtil.findByUuid(subjectTypes, request.getSubjectTypeUUID())); - groupPrivilege.setProgram(CollectionUtil.findByUuid(programs, request.getProgramUUID())); - groupPrivilege.setEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getEncounterTypeUUID())); - groupPrivilege.setProgramEncounterType(CollectionUtil.findByUuid(encounterTypes, request.getProgramEncounterTypeUUID())); - groupPrivilege.setChecklistDetail(CollectionUtil.findByUuid(checklistDetails, request.getChecklistDetailUUID())); + groupPrivilege.setPrivilege(privilegeMapByType.get(groupPrivilegeBundleContract.getPrivilegeType())); + groupPrivilege.setSubjectType(CollectionUtil.findByUuid(subjectTypes, groupPrivilegeBundleContract.getSubjectTypeUUID())); + groupPrivilege.setProgram(CollectionUtil.findByUuid(programs, groupPrivilegeBundleContract.getProgramUUID())); + groupPrivilege.setEncounterType(CollectionUtil.findByUuid(encounterTypes, groupPrivilegeBundleContract.getEncounterTypeUUID())); + groupPrivilege.setProgramEncounterType(CollectionUtil.findByUuid(encounterTypes, groupPrivilegeBundleContract.getProgramEncounterTypeUUID())); + groupPrivilege.setChecklistDetail(CollectionUtil.findByUuid(checklistDetails, groupPrivilegeBundleContract.getChecklistDetailUUID())); groupPrivilege.setGroup(targetedGroup); } - groupPrivilege.setAllow(request.isAllow()); + groupPrivilege.setAllow(groupPrivilegeBundleContract.isAllow()); groupPrivilegeRepository.saveGroupPrivilege(groupPrivilege); } catch (Exception e) { - throw new BulkItemSaveException(request, e); + throw new BulkItemSaveException(groupPrivilegeBundleContract, e); } }); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeBundleContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeBundleContract.java new file mode 100644 index 000000000..f682d9a46 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeBundleContract.java @@ -0,0 +1,34 @@ +package org.avni.server.web.request; + +import org.avni.server.domain.accessControl.GroupPrivilege; +import org.avni.server.domain.accessControl.PrivilegeType; + +public class GroupPrivilegeBundleContract extends GroupPrivilegeContractWeb { + private PrivilegeType privilegeType; + + public PrivilegeType getPrivilegeType() { + return privilegeType; + } + + public void setPrivilegeType(PrivilegeType privilegeType) { + this.privilegeType = privilegeType; + } + + public static GroupPrivilegeBundleContract fromEntity(GroupPrivilege groupPrivilege) { + GroupPrivilegeBundleContract groupPrivilegeContractWeb = new GroupPrivilegeBundleContract(); + groupPrivilegeContractWeb.setUuid(groupPrivilege.getUuid()); + groupPrivilegeContractWeb.setGroupUUID(groupPrivilege.getGroupUuid()); + groupPrivilegeContractWeb.setPrivilegeUUID(groupPrivilege.getPrivilegeUuid()); + groupPrivilegeContractWeb.setPrivilegeType(groupPrivilege.getPrivilege().getType()); + groupPrivilegeContractWeb.setSubjectTypeUUID(groupPrivilege.getSubjectTypeUuid()); + groupPrivilegeContractWeb.setProgramUUID(groupPrivilege.getProgramUuid()); + groupPrivilegeContractWeb.setProgramEncounterTypeUUID(groupPrivilege.getProgramEncounterTypeUuid()); + groupPrivilegeContractWeb.setEncounterTypeUUID(groupPrivilege.getEncounterTypeUuid()); + groupPrivilegeContractWeb.setChecklistDetailUUID(groupPrivilege.getChecklistDetailUuid()); + groupPrivilegeContractWeb.setAllow(groupPrivilege.isAllow()); + groupPrivilegeContractWeb.setVoided(groupPrivilege.isVoided()); + groupPrivilegeContractWeb.setNotEveryoneGroup(!groupPrivilege.getGroup().isEveryone()); + return groupPrivilegeContractWeb; + } + +} diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java b/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java index d6d7a14ed..69a24a9c7 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/GroupPrivilegeContractWeb.java @@ -1,7 +1,5 @@ package org.avni.server.web.request; -import org.avni.server.domain.accessControl.GroupPrivilege; - public class GroupPrivilegeContractWeb extends CHSRequest { private String groupUUID; private String privilegeUUID; @@ -13,22 +11,6 @@ public class GroupPrivilegeContractWeb extends CHSRequest { private boolean allow; private boolean isNotEveryoneGroup = true; - public static GroupPrivilegeContractWeb fromEntity(GroupPrivilege groupPrivilege) { - GroupPrivilegeContractWeb groupPrivilegeContractWeb = new GroupPrivilegeContractWeb(); - groupPrivilegeContractWeb.setUuid(groupPrivilege.getUuid()); - groupPrivilegeContractWeb.setGroupUUID(groupPrivilege.getGroupUuid()); - groupPrivilegeContractWeb.setPrivilegeUUID(groupPrivilege.getPrivilegeUuid()); - groupPrivilegeContractWeb.setSubjectTypeUUID(groupPrivilege.getSubjectTypeUuid()); - groupPrivilegeContractWeb.setProgramUUID(groupPrivilege.getProgramUuid()); - groupPrivilegeContractWeb.setProgramEncounterTypeUUID(groupPrivilege.getProgramEncounterTypeUuid()); - groupPrivilegeContractWeb.setEncounterTypeUUID(groupPrivilege.getEncounterTypeUuid()); - groupPrivilegeContractWeb.setChecklistDetailUUID(groupPrivilege.getChecklistDetailUuid()); - groupPrivilegeContractWeb.setAllow(groupPrivilege.isAllow()); - groupPrivilegeContractWeb.setVoided(groupPrivilege.isVoided()); - groupPrivilegeContractWeb.setNotEveryoneGroup(!groupPrivilege.getGroup().isEveryone()); - return groupPrivilegeContractWeb; - } - public String getGroupUUID() { return groupUUID; } From dae3c024203f97d98ffac533af253cd71f0b523a Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 14 Aug 2024 10:42:58 +0530 Subject: [PATCH 084/129] avniproject/avni-webapp#787 | Add check on deleteAdminConfig --- .../java/org/avni/server/web/ImplementationController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java index b7da6e33f..8286abf03 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ImplementationController.java @@ -114,6 +114,9 @@ public ResponseEntity delete(@Param("deleteMetadata") boolean deleteMetadata, if (OrganisationCategory.Production.equals(organisation.getCategory().getName())) { return new ResponseEntity<>("Production organisation's data cannot be deleted", HttpStatus.CONFLICT); } + if(deleteAdminConfig && !deleteMetadata) { + return new ResponseEntity<>("You cannot delete admin config data without deleting metadata", HttpStatus.BAD_REQUEST); + } //Delete checkPrivilegeAndDeleteTransactionalData(organisation); selectivelyCleanupMediaContent(deleteMetadata); From 787fb322036782e0b3405fe9445c8813c4bb28d7 Mon Sep 17 00:00:00 2001 From: Joy A Date: Wed, 14 Aug 2024 14:37:07 +0530 Subject: [PATCH 085/129] #770 | Create subjects for users within bundle upload job instead of separate job if bundle upload creates new user subject type --- .../UserSubjectTypeCreateTasklet.java | 13 +++---------- .../importer/batch/zip/BundleZipFileImporter.java | 2 +- .../org/avni/server/service/SubjectTypeService.java | 8 +++++--- .../java/org/avni/server/service/UserService.java | 5 +++++ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/userSubjectType/UserSubjectTypeCreateTasklet.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/userSubjectType/UserSubjectTypeCreateTasklet.java index ab061b1d5..c23f0d93d 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/userSubjectType/UserSubjectTypeCreateTasklet.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/userSubjectType/UserSubjectTypeCreateTasklet.java @@ -1,7 +1,7 @@ package org.avni.server.importer.batch.userSubjectType; -import org.avni.server.dao.*; -import org.avni.server.domain.*; +import org.avni.server.dao.SubjectTypeRepository; +import org.avni.server.domain.SubjectType; import org.avni.server.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,13 +15,11 @@ import org.springframework.stereotype.Component; import javax.transaction.Transactional; -import java.util.List; @Component @JobScope public class UserSubjectTypeCreateTasklet implements Tasklet { private final SubjectTypeRepository subjectTypeRepository; - private final UserRepository userRepository; private final UserService userService; private static final Logger logger = LoggerFactory.getLogger(UserSubjectTypeCreateTasklet.class); @@ -30,10 +28,8 @@ public class UserSubjectTypeCreateTasklet implements Tasklet { @Autowired public UserSubjectTypeCreateTasklet(SubjectTypeRepository subjectTypeRepository, - UserRepository userRepository, UserService userService) { this.subjectTypeRepository = subjectTypeRepository; - this.userRepository = userRepository; this.userService = userService; } @@ -42,10 +38,7 @@ public UserSubjectTypeCreateTasklet(SubjectTypeRepository subjectTypeRepository, public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) { try { SubjectType subjectType = subjectTypeRepository.findOne(subjectTypeId); - List users = userRepository.findAllByIsVoidedFalseAndOrganisationId(subjectType.getOrganisationId()); - users.forEach(user -> { - userService.ensureSubjectForUser(user, subjectType); - }); + userService.ensureSubjectsForUserSubjectType(subjectType); } catch (Exception e) { logger.error(e.getMessage(), e); } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java index 85380d555..42772c6e2 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java @@ -271,7 +271,7 @@ private void deployFile(String fileName, String fileData, List users = userRepository.findAllByIsVoidedFalseAndOrganisationId(subjectType.getOrganisationId()); + users.forEach(user -> ensureSubjectForUser(user, subjectType)); + } + public void ensureSubjectForUser(User user, SubjectType subjectType) { if (!subjectType.getType().equals(Subject.User)) throw new RuntimeException(String.format("Subject type: %s is not of User type", subjectType.getType())); From 19ad37c811a7760fbbc4100b13c06e07a216f184 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 14 Aug 2024 15:00:12 +0530 Subject: [PATCH 086/129] avniproject/avni-webapp#1298 - null is expected not exception --- .../src/main/java/org/avni/server/dao/CHSRepository.java | 5 +++++ .../main/java/org/avni/server/web/IndividualController.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/CHSRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/CHSRepository.java index 5da57c6d9..106abe27a 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/CHSRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/CHSRepository.java @@ -22,6 +22,11 @@ default T findEntity(Long id) { return findById(id).orElse(null); } + default T findEntity(String uuid) { + if (uuid == null) return null; + return findByUuid(uuid); + } + default Predicate jsonContains(Path jsonb, String pattern, CriteriaBuilder builder) { return builder.isTrue(builder.function("jsonb_object_values_contain", Boolean.class, jsonb, builder.literal(pattern))); diff --git a/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java b/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java index eae084d01..17243aad6 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/IndividualController.java @@ -354,7 +354,7 @@ public Resource process(Resource resource) { public AvniEntityResponse saveForWeb(@RequestBody IndividualRequest individualRequest) { try { logger.info(String.format("Saving individual with UUID %s", individualRequest.getUuid())); - Individual savedIndividual = individualService.getIndividual(individualRequest.getUuid()); + Individual savedIndividual = individualRepository.findEntity(individualRequest.getUuid()); //Subject is changed after this line, hence the following line cannot be moved down closer to its usage SubjectPartitionData subjectPartitionData = SubjectPartitionData.create(savedIndividual); From 84ec00b8575fda89860e8b7533065b6386dd4ccf Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 14 Aug 2024 18:18:33 +0530 Subject: [PATCH 087/129] #707 | Use trimmed values while save/update of user "name" and "username" fields --- .../java/org/avni/server/web/UserController.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserController.java b/avni-server-api/src/main/java/org/avni/server/web/UserController.java index dcf1ca186..cc77e98c7 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserController.java @@ -56,8 +56,6 @@ public class UserController { private final AccountAdminRepository accountAdminRepository; private final ResetSyncService resetSyncService; private final SubjectTypeRepository subjectTypeRepository; - - private final Pattern NAME_INVALID_CHARS_PATTERN = Pattern.compile("^.*[<>=\"].*$"); private final AccessControlService accessControlService; @Autowired @@ -93,14 +91,19 @@ private Boolean usernameExists(String name) { public ResponseEntity createUser(@RequestBody UserContract userContract) { accessControlService.checkPrivilege(PrivilegeType.EditUserConfiguration); try { - if (usernameExists(userContract.getUsername())) + if (isUserNameInvalid(userContract.getUsername())) { + throw new ValidationException(String.format("Invalid username %s", userContract.getUsername())); + } + + if (usernameExists(userContract.getUsername().trim())) { throw new ValidationException(String.format("Username %s already exists", userContract.getUsername())); + } User user = new User(); user.setUuid(UUID.randomUUID().toString()); logger.info(String.format("Creating user with username '%s' and UUID '%s'", userContract.getUsername(), user.getUuid())); - user.setUsername(userContract.getUsername()); + user.setUsername(userContract.getUsername().trim()); user = setUserAttributes(user, userContract, getRegionForUser(userContract)); User savedUser = userService.save(user); @@ -178,7 +181,7 @@ private Boolean isUserNameInvalid(String userName) { } private Boolean isNameInvalid(String name) { - return ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(name, NAME_INVALID_CHARS_PATTERN); + return ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(name, ValidationUtil.COMMON_INVALID_CHARS_PATTERN); } private User setUserAttributes(User user, UserContract userContract, String userRegion) { @@ -196,7 +199,7 @@ private User setUserAttributes(User user, UserContract userContract, String user throw new ValidationException(String.format("Invalid name %s", userContract.getName())); } - user.setName(userContract.getName()); + user.setName(userContract.getName().trim()); if (userContract.getCatchmentId() != null) { user.setCatchment(catchmentRepository.findOne(userContract.getCatchmentId())); } From bba592577f3a4ced8cbc7e918cc41c37e6cc764a Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 14 Aug 2024 18:24:16 +0530 Subject: [PATCH 088/129] #707 | Revert change to Name Invalid Values pattern --- .../src/main/java/org/avni/server/web/UserController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserController.java b/avni-server-api/src/main/java/org/avni/server/web/UserController.java index cc77e98c7..9e9688856 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserController.java @@ -58,6 +58,8 @@ public class UserController { private final SubjectTypeRepository subjectTypeRepository; private final AccessControlService accessControlService; + private final Pattern NAME_INVALID_CHARS_PATTERN = Pattern.compile("^.*[<>=\"].*$"); + @Autowired public UserController(CatchmentRepository catchmentRepository, UserRepository userRepository, @@ -181,7 +183,7 @@ private Boolean isUserNameInvalid(String userName) { } private Boolean isNameInvalid(String name) { - return ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(name, ValidationUtil.COMMON_INVALID_CHARS_PATTERN); + return ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(name, NAME_INVALID_CHARS_PATTERN); } private User setUserAttributes(User user, UserContract userContract, String userRegion) { From 3ac60d2e115d70d6a7c1ef6134cb4046c9157bc0 Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 14 Aug 2024 18:39:45 +0530 Subject: [PATCH 089/129] #707 | Add validations on name and username for User Upload --- .../java/org/avni/server/domain/User.java | 23 ++++++++++++++----- .../csv/writer/UserAndCatchmentWriter.java | 7 +++--- .../org/avni/server/util/ValidationUtil.java | 1 + .../org/avni/server/web/UserController.java | 4 +--- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/User.java b/avni-server-api/src/main/java/org/avni/server/domain/User.java index d08b8cf1e..c0df6e15a 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/User.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/User.java @@ -1,18 +1,17 @@ package org.avni.server.domain; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import com.google.i18n.phonenumbers.Phonenumber; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.validator.routines.EmailValidator; import org.avni.server.util.ObjectMapperSingleton; +import org.avni.server.util.ValidationUtil; import org.avni.server.web.request.syncAttribute.UserSyncSettings; +import org.avni.server.web.validation.ValidationException; import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.Type; import org.hibernate.proxy.HibernateProxyHelper; import org.joda.time.DateTime; -import org.avni.server.web.validation.ValidationException; import javax.persistence.*; import javax.validation.constraints.NotNull; @@ -380,14 +379,26 @@ public static void validateEmail(String email) { * where yyy is {@link Organisation#getUsernameSuffix()} and xxx represents user */ public static void validateUsername(String username, String userSuffix) { - if (username == null || !username.contains("@") || username.length() < 7) { + if (username == null || !username.contains("@") || username.trim().length() < 7) { throw new ValidationException(String.format("Invalid username '%s'. It must be at least 7 characters.", username)); } - if (username.indexOf("@") < 4) { + if (username.trim().indexOf("@") < 4) { throw new ValidationException(String.format("Invalid username '%s'. Name part should be at least 4 characters", username)); } - if (!username.endsWith(userSuffix)) { + if (!username.trim().endsWith(userSuffix)) { throw new ValidationException(String.format("Invalid username '%s'. Include correct userSuffix %s at the end", username, userSuffix)); } + if (ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(username.trim(), ValidationUtil.COMMON_INVALID_CHARS_PATTERN)) { + throw new ValidationException(String.format("Invalid username '%s', contains atleast one disallowed character %s", username, ValidationUtil.COMMON_INVALID_CHARS_PATTERN)); + } + } + + /** + * name must not be empty and not have invalid characters + */ + public static void validateName(String name) { + if (ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(name, ValidationUtil.NAME_INVALID_CHARS_PATTERN)) { + throw new ValidationException(String.format("Invalid name '%s', contains atleast one disallowed character %s", name, ValidationUtil.NAME_INVALID_CHARS_PATTERN)); + } } } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index 155f76200..7e4568436 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -87,19 +87,20 @@ private void write(Row row) throws Exception { Organisation organisation = UserContextHolder.getUserContext().getOrganisation(); String userSuffix = "@".concat(organisation.getEffectiveUsernameSuffix()); User.validateUsername(username, userSuffix); - User user = userRepository.findByUsername(username); + User user = userRepository.findByUsername(username.trim()); User currentUser = userService.getCurrentUser(); boolean isNewUser = false; if (user == null) { user = new User(); user.assignUUIDIfRequired(); - user.setUsername(username); + user.setUsername(username.trim()); isNewUser = true; } User.validateEmail(email); user.setEmail(email); userService.setPhoneNumber(phoneNumber, user, RegionUtil.getCurrentUserRegion()); - user.setName(nameOfUser); + User.validateName(nameOfUser); + user.setName(nameOfUser.trim()); if (!isNewUser) resetSyncService.recordSyncAttributeValueChangeForUser(user, catchment.getId(), syncSettings); user.setCatchment(catchment); user.setOperatingIndividualScope(ByCatchment); diff --git a/avni-server-api/src/main/java/org/avni/server/util/ValidationUtil.java b/avni-server-api/src/main/java/org/avni/server/util/ValidationUtil.java index 244029b64..f22abe941 100644 --- a/avni-server-api/src/main/java/org/avni/server/util/ValidationUtil.java +++ b/avni-server-api/src/main/java/org/avni/server/util/ValidationUtil.java @@ -5,6 +5,7 @@ public class ValidationUtil { public static final Pattern COMMON_INVALID_CHARS_PATTERN = Pattern.compile("^.*[<>=\"'].*$"); + public static final Pattern NAME_INVALID_CHARS_PATTERN = Pattern.compile("^.*[<>=\"].*$"); public static boolean checkNull(Object checkObject) { return checkObject == null; diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserController.java b/avni-server-api/src/main/java/org/avni/server/web/UserController.java index 9e9688856..db0f915cb 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserController.java @@ -58,8 +58,6 @@ public class UserController { private final SubjectTypeRepository subjectTypeRepository; private final AccessControlService accessControlService; - private final Pattern NAME_INVALID_CHARS_PATTERN = Pattern.compile("^.*[<>=\"].*$"); - @Autowired public UserController(CatchmentRepository catchmentRepository, UserRepository userRepository, @@ -183,7 +181,7 @@ private Boolean isUserNameInvalid(String userName) { } private Boolean isNameInvalid(String name) { - return ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(name, NAME_INVALID_CHARS_PATTERN); + return ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(name, ValidationUtil.NAME_INVALID_CHARS_PATTERN); } private User setUserAttributes(User user, UserContract userContract, String userRegion) { From cfa7ca3901da126dbff0586122a1244c2b3e9694 Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 16 Aug 2024 15:57:36 +0530 Subject: [PATCH 090/129] avniproject/avni-webapp#1214 | Add validations for Language and DatePickerMode during UserAndCatchment file upload --- .../java/org/avni/server/domain/JsonObject.java | 4 ++-- .../batch/csv/writer/UserAndCatchmentWriter.java | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/domain/JsonObject.java b/avni-server-api/src/main/java/org/avni/server/domain/JsonObject.java index a370679fc..08df8bdf1 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/JsonObject.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/JsonObject.java @@ -21,9 +21,9 @@ public JsonObject with(String key, Object value) { return this; } - public JsonObject withEmptyCheck(String key, String value){ + public JsonObject withEmptyCheckAndTrim(String key, String value){ if(!S.isEmpty(value)){ - super.put(key, value); + super.put(key, value.trim()); } return this; } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index 7e4568436..c8f20380c 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -36,6 +36,7 @@ public class UserAndCatchmentWriter implements ItemWriter, Serializable { private final Pattern compoundHeaderPattern; private final ResetSyncService resetSyncService; private static final String METADATA_ROW_START_STRING = "Mandatory field."; + private static final List DATE_PICKER_MODE_OPTIONS = Arrays.asList("calendar", "spinner"); @Autowired public UserAndCatchmentWriter(CatchmentService catchmentService, @@ -79,6 +80,14 @@ private void write(Row row) throws Exception { String groupsSpecified = row.get("User Groups"); JsonObject syncSettings = constructSyncSettings(row); + if(Objects.isNull(locale)) { + throw new Exception(format("Provided value '%s' for Preferred Language is invalid;", language)); + } + + if(Objects.isNull(datePickerMode) || !DATE_PICKER_MODE_OPTIONS.contains(datePickerMode)) { + throw new Exception(format("Provided value '%s' for Date picker mode is invalid;", datePickerMode)); + } + AddressLevel location = locationRepository.findByTitleLineageIgnoreCase(fullAddress) .orElseThrow(() -> new Exception(format( "Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'", fullAddress))); @@ -109,9 +118,9 @@ private void write(Row row) throws Exception { user.setSettings(new JsonObject() .with("locale", locale) .with("trackLocation", trackLocation) - .withEmptyCheck("datePickerMode", datePickerMode) + .withEmptyCheckAndTrim("datePickerMode", datePickerMode) .with("showBeneficiaryMode", beneficiaryMode) - .withEmptyCheck(UserSettings.ID_PREFIX, idPrefix)); + .withEmptyCheckAndTrim(UserSettings.ID_PREFIX, idPrefix)); user.setOrganisationId(organisation.getId()); user.setAuditInfo(currentUser); From 7942e7cdf11f5f37e23cbe39d3635aaa6c5e3bdb Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 16 Aug 2024 16:24:08 +0530 Subject: [PATCH 091/129] avniproject/avni-webapp#1214 | Revert inadvertent code change due to duplicate warning --- .../server/service/SubjectTypeService.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java b/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java index c2c22fee6..58c0d0296 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java @@ -234,7 +234,25 @@ private List constructSyncAttributeHeadersForSubjectType(SubjectType sub } public List constructSyncAttributeAllowedValuesForSubjectTypes() { - return this.constructSyncAttributeHeadersForSubjectTypes(); + List subjectTypes = subjectTypeRepository.findByIsVoidedFalse(); + Predicate subjectTypeHasSyncAttributes = subjectType -> + Objects.nonNull(subjectType.getSyncRegistrationConcept1()) || + Objects.nonNull(subjectType.getSyncRegistrationConcept2()); + return subjectTypes.stream().sorted((a,b) -> (int) (a.getId() - b.getId())). + filter(subjectTypeHasSyncAttributes). + map(this::constructSyncAttributeAllowedValuesForSubjectType). + flatMap(Collection::stream). + collect(Collectors.toList()); + } + + private List constructSyncAttributeAllowedValuesForSubjectType(SubjectType subjectTypeWithSyncAttribute) { + String[] syncAttributes = new String[]{subjectTypeWithSyncAttribute.getSyncRegistrationConcept1(), + subjectTypeWithSyncAttribute.getSyncRegistrationConcept2()}; + + return Arrays.stream(syncAttributes). + filter(Objects::nonNull).sorted(). + map(sa -> String.format("\"Allowed values: %s\"", conceptService.getSampleValuesForSyncConcept(conceptService.get(sa)))) + .collect(Collectors.toList()); } public JsonObject getDefaultSettings() { From fb90c13785bb40b14b8a6a8fa09782e2a8bb6cfb Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 16 Aug 2024 16:24:59 +0530 Subject: [PATCH 092/129] avniproject/avni-webapp#1214 | Include Mandatory field prefix --- .../main/java/org/avni/server/service/SubjectTypeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java b/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java index 58c0d0296..6690758af 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/SubjectTypeService.java @@ -251,7 +251,7 @@ private List constructSyncAttributeAllowedValuesForSubjectType(SubjectTy return Arrays.stream(syncAttributes). filter(Objects::nonNull).sorted(). - map(sa -> String.format("\"Allowed values: %s\"", conceptService.getSampleValuesForSyncConcept(conceptService.get(sa)))) + map(sa -> String.format("\"Mandatory field. Allowed values: %s\"", conceptService.getSampleValuesForSyncConcept(conceptService.get(sa)))) .collect(Collectors.toList()); } From 6a9c490bd99e0525d6b23aaed69b1e768167acc1 Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 16 Aug 2024 19:53:41 +0530 Subject: [PATCH 093/129] avniproject/avni-webapp#1214 | Add validation for mandatory headers and their column values --- .../csv/writer/UserAndCatchmentWriter.java | 90 ++++++++++++++----- .../header/UsersAndCatchmentsHeaders.java | 26 ++++++ 2 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/header/UsersAndCatchmentsHeaders.java diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index c8f20380c..bb8b48e2b 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -2,17 +2,20 @@ import org.avni.server.dao.LocationRepository; import org.avni.server.dao.UserRepository; -import org.avni.server.domain.*; import org.avni.server.domain.Locale; +import org.avni.server.domain.*; import org.avni.server.framework.security.UserContextHolder; +import org.avni.server.importer.batch.csv.writer.header.UsersAndCatchmentsHeaders; import org.avni.server.importer.batch.model.Row; import org.avni.server.service.*; import org.avni.server.util.RegionUtil; import org.avni.server.util.S; import org.avni.server.web.request.syncAttribute.UserSyncSettings; +import org.avni.server.web.validation.ValidationException; import org.springframework.batch.item.ItemWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.io.Serializable; @@ -22,6 +25,7 @@ import static java.lang.String.format; import static org.avni.server.domain.OperatingIndividualScope.ByCatchment; +import static org.avni.server.importer.batch.csv.writer.header.UsersAndCatchmentsHeaders.*; @Component public class UserAndCatchmentWriter implements ItemWriter, Serializable { @@ -60,38 +64,82 @@ public UserAndCatchmentWriter(CatchmentService catchmentService, @Override public void write(List rows) throws Exception { - for (Row row : rows) write(row); + if(!CollectionUtils.isEmpty(rows)) { + validateHeaders(rows.get(0).getHeaders()); + for (Row row : rows) write(row); + } + } + + private void validateHeaders(String[] headers) { + List headerList = Arrays.asList(headers); + List allErrorMsgs = new ArrayList<>(); + UsersAndCatchmentsHeaders usersAndCatchmentsHeaders = new UsersAndCatchmentsHeaders(); + List expectedStandardHeaders = Arrays.asList(usersAndCatchmentsHeaders.getAllHeaders()); + List syncAttributeHeadersForSubjectTypes = subjectTypeService.constructSyncAttributeHeadersForSubjectTypes(); + checkForMissingHeaders(headerList, allErrorMsgs, expectedStandardHeaders, syncAttributeHeadersForSubjectTypes); + checkForUnknownHeaders(headerList, allErrorMsgs, expectedStandardHeaders, syncAttributeHeadersForSubjectTypes); + if(!allErrorMsgs.isEmpty()) { + throw new RuntimeException(String.join(", ", allErrorMsgs)); + } + } + + private void checkForUnknownHeaders(List headerList, List allErrorMsgs, List expectedStandardHeaders, List syncAttributeHeadersForSubjectTypes) { + headerList.removeIf(StringUtils::isEmpty); + headerList.removeIf(header -> expectedStandardHeaders.contains(header)); + headerList.removeIf(header -> syncAttributeHeadersForSubjectTypes.contains(header)); + if (!headerList.isEmpty()) { + allErrorMsgs.add("Unknown headers included in file. Please refer to sample file for valid list of headers."); + } + } + + private void checkForMissingHeaders(List headerList, List allErrorMsgs, List expectedStandardHeaders, List syncAttributeHeadersForSubjectTypes) { + if (headerList.isEmpty() || !headerList.containsAll(expectedStandardHeaders) || !headerList.containsAll(syncAttributeHeadersForSubjectTypes)) { + allErrorMsgs.add("Mandatory columns are missing from uploaded file. Please refer to sample file for the list of mandatory headers."); + } } private void write(Row row) throws Exception { - String fullAddress = row.get("Location with full hierarchy"); + String fullAddress = row.get(LOCATION_WITH_FULL_HIERARCHY); if (fullAddress != null && fullAddress.startsWith(METADATA_ROW_START_STRING)) return; - String catchmentName = row.get("Catchment Name"); - String nameOfUser = row.get("Full Name of User"); - String username = row.get("Username"); - String email = row.get("Email Address"); - String phoneNumber = row.get("Mobile Number"); - String language = row.get("Preferred Language"); + String catchmentName = row.get(CATCHMENT_NAME); + String nameOfUser = row.get(FULL_NAME_OF_USER); + String username = row.get(USERNAME); + String email = row.get(EMAIL_ADDRESS); + String phoneNumber = row.get(MOBILE_NUMBER); + String language = row.get(PREFERRED_LANGUAGE); Locale locale = S.isEmpty(language) ? Locale.en : Locale.valueByName(language); - Boolean trackLocation = row.getBool("Track Location"); - String datePickerMode = row.get("Date picker mode"); - Boolean beneficiaryMode = row.getBool("Enable Beneficiary mode"); - String idPrefix = row.get("Identifier Prefix"); - String groupsSpecified = row.get("User Groups"); + Boolean trackLocation = row.getBool(TRACK_LOCATION); + String datePickerMode = row.get(DATE_PICKER_MODE); + Boolean beneficiaryMode = row.getBool(ENABLE_BENEFICIARY_MODE); + String idPrefix = row.get(IDENTIFIER_PREFIX); + String groupsSpecified = row.get(USER_GROUPS); JsonObject syncSettings = constructSyncSettings(row); + AddressLevel location = locationRepository.findByTitleLineageIgnoreCase(fullAddress) + .orElseThrow(() -> new Exception(format( + "Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'", fullAddress))); + if(!StringUtils.hasLength(catchmentName)) { + throw new Exception(format("Invalid or Empty value specified for mandatory field %s", CATCHMENT_NAME)); + } + if(!StringUtils.hasLength(username)) { + throw new Exception(format("Invalid or Empty value specified for mandatory field %s", USERNAME)); + } + if(!StringUtils.hasLength(nameOfUser)) { + throw new Exception(format("Invalid or Empty value specified for mandatory field %s", FULL_NAME_OF_USER)); + } + if(!StringUtils.hasLength(email)) { + throw new Exception(format("Invalid or Empty value specified for mandatory field %s", EMAIL_ADDRESS)); + } + if(!StringUtils.hasLength(phoneNumber)) { + throw new Exception(format("Invalid or Empty value specified for mandatory field %s", MOBILE_NUMBER)); + } if(Objects.isNull(locale)) { throw new Exception(format("Provided value '%s' for Preferred Language is invalid;", language)); } - if(Objects.isNull(datePickerMode) || !DATE_PICKER_MODE_OPTIONS.contains(datePickerMode)) { throw new Exception(format("Provided value '%s' for Date picker mode is invalid;", datePickerMode)); } - AddressLevel location = locationRepository.findByTitleLineageIgnoreCase(fullAddress) - .orElseThrow(() -> new Exception(format( - "Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'", fullAddress))); - Catchment catchment = catchmentService.createOrUpdate(catchmentName, location); Organisation organisation = UserContextHolder.getUserContext().getOrganisation(); String userSuffix = "@".concat(organisation.getEffectiveUsernameSuffix()); @@ -153,7 +201,9 @@ private void updateSyncSettingsFor(String saHeader, Row row, Map headers = new ArrayList<>(Arrays.asList(LOCATION_WITH_FULL_HIERARCHY, CATCHMENT_NAME, USERNAME, FULL_NAME_OF_USER, EMAIL_ADDRESS, MOBILE_NUMBER, PREFERRED_LANGUAGE, TRACK_LOCATION, DATE_PICKER_MODE, ENABLE_BENEFICIARY_MODE, IDENTIFIER_PREFIX, USER_GROUPS)); + return headers.toArray(new String[0]); + } +} From df919ca260054df4c1a7b823bd5a4779fbb36a79 Mon Sep 17 00:00:00 2001 From: himeshr Date: Mon, 19 Aug 2024 13:19:20 +0530 Subject: [PATCH 094/129] avniproject/avni-webapp#1214 | assimilate all validation errors for a row before throwing exception --- .../csv/writer/UserAndCatchmentWriter.java | 131 +++++++++++------- 1 file changed, 81 insertions(+), 50 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index bb8b48e2b..ba4cfa024 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -11,7 +11,6 @@ import org.avni.server.util.RegionUtil; import org.avni.server.util.S; import org.avni.server.web.request.syncAttribute.UserSyncSettings; -import org.avni.server.web.validation.ValidationException; import org.springframework.batch.item.ItemWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -39,6 +38,15 @@ public class UserAndCatchmentWriter implements ItemWriter, Serializable { private final ConceptService conceptService; private final Pattern compoundHeaderPattern; private final ResetSyncService resetSyncService; + + private static final String ERR_MSG_MANDATORY_FIELD = "Invalid or Empty value specified for mandatory field %s"; + private static final String ERR_MSG_LOCATION_FIELD = "Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'"; + private static final String ERR_MSG_LOCALE_FIELD = "Provided value '%s' for Preferred Language is invalid"; + private static final String ERR_MSG_DATE_PICKER_FIELD = "Provided value '%s' for Date picker mode is invalid"; + private static final String ERR_MSG_UNKNOWN_HEADERS = "Unknown headers included in file. Please refer to sample file for valid list of headers"; + private static final String ERR_MSG_MISSING_MANDATORY_FIELDS = "Mandatory columns are missing from uploaded file. Please refer to sample file for the list of mandatory headers."; + private static final String ERR_MSG_INVALID_CONCEPT_ANSWER = "'%s' is not a valid value for the concept '%s'" + + "To input this value, add this as an answer to the coded concept '%s'"; private static final String METADATA_ROW_START_STRING = "Mandatory field."; private static final List DATE_PICKER_MODE_OPTIONS = Arrays.asList("calendar", "spinner"); @@ -88,17 +96,18 @@ private void checkForUnknownHeaders(List headerList, List allErr headerList.removeIf(header -> expectedStandardHeaders.contains(header)); headerList.removeIf(header -> syncAttributeHeadersForSubjectTypes.contains(header)); if (!headerList.isEmpty()) { - allErrorMsgs.add("Unknown headers included in file. Please refer to sample file for valid list of headers."); + allErrorMsgs.add(ERR_MSG_UNKNOWN_HEADERS); } } private void checkForMissingHeaders(List headerList, List allErrorMsgs, List expectedStandardHeaders, List syncAttributeHeadersForSubjectTypes) { if (headerList.isEmpty() || !headerList.containsAll(expectedStandardHeaders) || !headerList.containsAll(syncAttributeHeadersForSubjectTypes)) { - allErrorMsgs.add("Mandatory columns are missing from uploaded file. Please refer to sample file for the list of mandatory headers."); + allErrorMsgs.add(ERR_MSG_MISSING_MANDATORY_FIELDS); } } private void write(Row row) throws Exception { + List rowValidationErrorMsgs = new ArrayList<>(); String fullAddress = row.get(LOCATION_WITH_FULL_HIERARCHY); if (fullAddress != null && fullAddress.startsWith(METADATA_ROW_START_STRING)) return; String catchmentName = row.get(CATCHMENT_NAME); @@ -107,43 +116,17 @@ private void write(Row row) throws Exception { String email = row.get(EMAIL_ADDRESS); String phoneNumber = row.get(MOBILE_NUMBER); String language = row.get(PREFERRED_LANGUAGE); - Locale locale = S.isEmpty(language) ? Locale.en : Locale.valueByName(language); Boolean trackLocation = row.getBool(TRACK_LOCATION); String datePickerMode = row.get(DATE_PICKER_MODE); Boolean beneficiaryMode = row.getBool(ENABLE_BENEFICIARY_MODE); String idPrefix = row.get(IDENTIFIER_PREFIX); String groupsSpecified = row.get(USER_GROUPS); - JsonObject syncSettings = constructSyncSettings(row); - - AddressLevel location = locationRepository.findByTitleLineageIgnoreCase(fullAddress) - .orElseThrow(() -> new Exception(format( - "Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'", fullAddress))); - if(!StringUtils.hasLength(catchmentName)) { - throw new Exception(format("Invalid or Empty value specified for mandatory field %s", CATCHMENT_NAME)); - } - if(!StringUtils.hasLength(username)) { - throw new Exception(format("Invalid or Empty value specified for mandatory field %s", USERNAME)); - } - if(!StringUtils.hasLength(nameOfUser)) { - throw new Exception(format("Invalid or Empty value specified for mandatory field %s", FULL_NAME_OF_USER)); - } - if(!StringUtils.hasLength(email)) { - throw new Exception(format("Invalid or Empty value specified for mandatory field %s", EMAIL_ADDRESS)); - } - if(!StringUtils.hasLength(phoneNumber)) { - throw new Exception(format("Invalid or Empty value specified for mandatory field %s", MOBILE_NUMBER)); - } - if(Objects.isNull(locale)) { - throw new Exception(format("Provided value '%s' for Preferred Language is invalid;", language)); - } - if(Objects.isNull(datePickerMode) || !DATE_PICKER_MODE_OPTIONS.contains(datePickerMode)) { - throw new Exception(format("Provided value '%s' for Date picker mode is invalid;", datePickerMode)); - } - - Catchment catchment = catchmentService.createOrUpdate(catchmentName, location); + AddressLevel location = locationRepository.findByTitleLineageIgnoreCase(fullAddress).orElse(null); + Locale locale = S.isEmpty(language) ? Locale.en : Locale.valueByName(language); Organisation organisation = UserContextHolder.getUserContext().getOrganisation(); String userSuffix = "@".concat(organisation.getEffectiveUsernameSuffix()); - User.validateUsername(username, userSuffix); + validateRowAndAssimilateErrors(rowValidationErrorMsgs, fullAddress, catchmentName, nameOfUser, username, email, phoneNumber, language, datePickerMode, location, locale, userSuffix); + Catchment catchment = catchmentService.createOrUpdate(catchmentName, location); User user = userRepository.findByUsername(username.trim()); User currentUser = userService.getCurrentUser(); boolean isNewUser = false; @@ -153,11 +136,10 @@ private void write(Row row) throws Exception { user.setUsername(username.trim()); isNewUser = true; } - User.validateEmail(email); user.setEmail(email); userService.setPhoneNumber(phoneNumber, user, RegionUtil.getCurrentUserRegion()); - User.validateName(nameOfUser); user.setName(nameOfUser.trim()); + JsonObject syncSettings = constructSyncSettings(row, rowValidationErrorMsgs); if (!isNewUser) resetSyncService.recordSyncAttributeValueChangeForUser(user, catchment.getId(), syncSettings); user.setCatchment(catchment); user.setOperatingIndividualScope(ByCatchment); @@ -181,11 +163,61 @@ private void write(Row row) throws Exception { } } - private JsonObject constructSyncSettings(Row row) { + private void validateRowAndAssimilateErrors(List rowValidationErrorMsgs, String fullAddress, String catchmentName, String nameOfUser, String username, String email, String phoneNumber, String language, String datePickerMode, AddressLevel location, Locale locale, String userSuffix) { + addErrMsgIfValidationFails(!StringUtils.hasLength(catchmentName), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, CATCHMENT_NAME)); + addErrMsgIfValidationFails(!StringUtils.hasLength(username), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, USERNAME)); + addErrMsgIfValidationFails(!StringUtils.hasLength(nameOfUser), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, FULL_NAME_OF_USER)); + addErrMsgIfValidationFails(!StringUtils.hasLength(email), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, EMAIL_ADDRESS)); + addErrMsgIfValidationFails(!StringUtils.hasLength(phoneNumber), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, MOBILE_NUMBER)); + + addErrMsgIfValidationFails(Objects.isNull(location), rowValidationErrorMsgs, format(ERR_MSG_LOCATION_FIELD, fullAddress)); + addErrMsgIfValidationFails(Objects.isNull(locale), rowValidationErrorMsgs, format(ERR_MSG_LOCALE_FIELD, language)); + addErrMsgIfValidationFails(Objects.isNull(datePickerMode) || !DATE_PICKER_MODE_OPTIONS.contains(datePickerMode), + rowValidationErrorMsgs, format(ERR_MSG_DATE_PICKER_FIELD, datePickerMode)); + + extractUserUsernameValidationErrMsg(rowValidationErrorMsgs, username, userSuffix); + extractUserNameValidationErrMsg(rowValidationErrorMsgs, nameOfUser); + extractUserEmailValidationErrMsg(rowValidationErrorMsgs, email); + if(!rowValidationErrorMsgs.isEmpty()) { + throw new RuntimeException(String.join(", ", rowValidationErrorMsgs)); + } + } + + private void extractUserNameValidationErrMsg(List rowValidationErrorMsgs, String nameOfUser) { + try { + User.validateName(nameOfUser); + } catch (Exception exception) { + addErrMsgIfValidationFails(true, rowValidationErrorMsgs, exception.getMessage()); + } + } + + private void extractUserEmailValidationErrMsg(List rowValidationErrorMsgs, String email) { + try { + User.validateEmail(email); + } catch (Exception exception) { + addErrMsgIfValidationFails(true, rowValidationErrorMsgs, exception.getMessage()); + } + } + + private void extractUserUsernameValidationErrMsg(List rowValidationErrorMsgs, String username, String userSuffix) { + try { + User.validateUsername(username, userSuffix); + } catch (Exception exception) { + addErrMsgIfValidationFails(true, rowValidationErrorMsgs, exception.getMessage()); + } + } + + private void addErrMsgIfValidationFails(boolean validationCheckResult, List rowValidationErrorMsgs, String validationErrorMessage) { + if(validationCheckResult) { + rowValidationErrorMsgs.add(validationErrorMessage); + } + } + + private JsonObject constructSyncSettings(Row row, List rowValidationErrorMsgs) { List syncAttributeHeadersForSubjectTypes = subjectTypeService.constructSyncAttributeHeadersForSubjectTypes(); Map syncSettingsMap = new HashMap<>(); for (String saHeader : syncAttributeHeadersForSubjectTypes) { - updateSyncSettingsFor(saHeader, row, syncSettingsMap); + updateSyncSettingsFor(saHeader, row, syncSettingsMap, rowValidationErrorMsgs); } JsonObject syncSettings = new JsonObject(); @@ -196,20 +228,18 @@ private JsonObject constructSyncSettings(Row row) { return syncSettings; } - private void updateSyncSettingsFor(String saHeader, Row row, Map syncSettingsMap) { + private void updateSyncSettingsFor(String saHeader, Row row, Map syncSettingsMap, List rowValidationErrorMsgs) { Matcher headerPatternMatcher = compoundHeaderPattern.matcher(saHeader); if (headerPatternMatcher.matches()) { String conceptName = headerPatternMatcher.group("conceptName"); String conceptValues = row.get(saHeader); - if (StringUtils.isEmpty(conceptValues)) { - throw new ValidationException(String.format("Invalid or Empty value specified for mandatory field %s", saHeader)); - } + addErrMsgIfValidationFails(StringUtils.isEmpty(conceptValues), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, saHeader)); String subjectTypeName = headerPatternMatcher.group("subjectTypeName"); SubjectType subjectType = subjectTypeService.getByName(subjectTypeName); UserSyncSettings userSyncSettings = syncSettingsMap.getOrDefault(subjectType.getUuid(), new UserSyncSettings()); updateSyncSubjectTypeSettings(subjectType, userSyncSettings); - updateSyncConceptSettings(subjectType, conceptName, conceptValues, userSyncSettings); + updateSyncConceptSettings(subjectType, conceptName, conceptValues, userSyncSettings, rowValidationErrorMsgs); syncSettingsMap.put(subjectType.getUuid(), userSyncSettings); } @@ -220,12 +250,12 @@ private void updateSyncSubjectTypeSettings(SubjectType subjectType, UserSyncSett userSyncSettings.setSubjectTypeUUID(subjectTypeUuid); } - private void updateSyncConceptSettings(SubjectType subjectType, String conceptName, String conceptValues, UserSyncSettings userSyncSettings) { + private void updateSyncConceptSettings(SubjectType subjectType, String conceptName, String conceptValues, UserSyncSettings userSyncSettings, List rowValidationErrorMsgs) { Concept concept = conceptService.getByName(conceptName); String conceptUuid = concept.getUuid(); List syncSettingsConceptRawValues = Arrays.asList(conceptValues.split(",")); List syncSettingsConceptProcessedValues = concept.isCoded() ? - findSyncSettingCodedConceptValues(syncSettingsConceptRawValues, concept) : syncSettingsConceptRawValues; + findSyncSettingCodedConceptValues(syncSettingsConceptRawValues, concept, rowValidationErrorMsgs) : syncSettingsConceptRawValues; String syncRegistrationConcept1 = subjectType.getSyncRegistrationConcept1(); if (syncRegistrationConcept1.equals(conceptUuid)) { @@ -237,16 +267,17 @@ private void updateSyncConceptSettings(SubjectType subjectType, String conceptNa } } - private List findSyncSettingCodedConceptValues(List syncSettingsValues, Concept concept) { + private List findSyncSettingCodedConceptValues(List syncSettingsValues, Concept concept, + List rowValidationErrorMsgs) { List syncSettingCodedConceptValues = new ArrayList<>(); for (String syncSettingsValue : syncSettingsValues) { Optional conceptAnswer = Optional.ofNullable(conceptService.getByName(syncSettingsValue)); - conceptAnswer.orElseThrow(() -> new RuntimeException(String.format("'%s' is not a valid value for the concept '%s'. " + - "To input this value, add this as an answer to the coded concept '%s'", - syncSettingsValue, concept.getName(), concept.getName()))); - syncSettingCodedConceptValues.add(conceptAnswer.get().getUuid()); + if(conceptAnswer.isPresent()) { + syncSettingCodedConceptValues.add(conceptAnswer.get().getUuid()); + } else { + rowValidationErrorMsgs.add(format(ERR_MSG_INVALID_CONCEPT_ANSWER, syncSettingsValue, concept.getName(), concept.getName())); + } } - return syncSettingCodedConceptValues; } } From bf16dde2bf3311c0af955550be1f117bf6489ef4 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 14 Aug 2024 14:29:40 +0530 Subject: [PATCH 095/129] avniproject/avni-webapp#1306 - WIP --- .../avni/server/service/ImportService.java | 32 +++++----- .../writer/LocationWriterIntegrationTest.java | 63 +++++++++++++++++++ .../service/builder/TestDataSetupService.java | 35 +++++++++++ 3 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java diff --git a/avni-server-api/src/main/java/org/avni/server/service/ImportService.java b/avni-server-api/src/main/java/org/avni/server/service/ImportService.java index dfe623f4d..6b74d46a3 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ImportService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ImportService.java @@ -26,7 +26,7 @@ import java.util.stream.Stream; @Service -public class ImportService implements ImportLocationsConstants{ +public class ImportService implements ImportLocationsConstants { private final SubjectTypeRepository subjectTypeRepository; private final FormMappingRepository formMappingRepository; @@ -123,7 +123,7 @@ public boolean isApprovalEnabled() { /** * Upload types can be - * + *

* Subject--- * ProgramEnrolment------ * ProgramEncounter------ @@ -138,7 +138,7 @@ public String getSampleFile(String uploadType) { String response = ""; if (uploadType.equals("usersAndCatchments")) { - return getUsersAndCatchmentsSampleFile(); + return getUsersAndCatchmentsSampleFile(); } if (uploadSpec[0].equals("Subject")) { @@ -171,22 +171,18 @@ public String getSampleFile(String uploadType) { public String getLocationsSampleFile(LocationWriter.LocationUploadMode locationUploadMode, String locationHierarchy) { StringBuilder sampleFileBuilder = new StringBuilder(); List addressLevelTypes = null; - try { - if (LocationWriter.LocationUploadMode.isCreateMode(locationUploadMode)) { - addressLevelTypes = getAddressLevelTypesForCreateModeSingleHierarchy(locationHierarchy); - } - List formElementNamesForLocationTypeFormElements = formService.getFormElementNamesForLocationTypeForms(); - appendHeaderRowForLocations(sampleFileBuilder, locationUploadMode, addressLevelTypes, formElementNamesForLocationTypeFormElements); - appendDescriptionForLocations(sampleFileBuilder, locationUploadMode, addressLevelTypes, formElementNamesForLocationTypeFormElements); - appendExamplesForLocations(sampleFileBuilder, locationUploadMode); - } catch (Exception e) { - throw new RuntimeException(e); + if (LocationWriter.LocationUploadMode.isCreateMode(locationUploadMode)) { + addressLevelTypes = getAddressLevelTypesForCreateModeSingleHierarchy(locationHierarchy); } + List formElementNamesForLocationTypeFormElements = formService.getFormElementNamesForLocationTypeForms(); + appendHeaderRowForLocations(sampleFileBuilder, locationUploadMode, addressLevelTypes, formElementNamesForLocationTypeFormElements); + appendDescriptionForLocations(sampleFileBuilder, locationUploadMode, addressLevelTypes, formElementNamesForLocationTypeFormElements); + appendExamplesForLocations(sampleFileBuilder, locationUploadMode); return sampleFileBuilder.toString(); } public List getAddressLevelTypesForCreateModeSingleHierarchy(String locationHierarchy) { - if(!StringUtils.hasText(locationHierarchy)) { + if (!StringUtils.hasText(locationHierarchy)) { throw new RuntimeException(String.format("Invalid value specified for locationHierarchy: %s", locationHierarchy)); } List selectedLocationHierarchy = Arrays.stream(locationHierarchy.split("\\.")) @@ -215,14 +211,14 @@ private void appendDescriptionForLocations(StringBuilder sampleFileBuilder, Loca List formElementNamesForLocationTypeFormElements) { if (LocationWriter.LocationUploadMode.isCreateMode(locationUploadMode)) { sampleFileBuilder.append(STRING_CONSTANT_NEW_LINE).append(addressLevelTypes.stream() - .map(alt -> String.format(STRING_PLACEHOLDER_BLOCK, Example+alt.getName()+ STRING_CONSTANT_ONE)).collect(Collectors.joining(STRING_CONSTANT_EMPTY_STRING))); + .map(alt -> String.format(STRING_PLACEHOLDER_BLOCK, Example + alt.getName() + STRING_CONSTANT_ONE)).collect(Collectors.joining(STRING_CONSTANT_EMPTY_STRING))); } else { sampleFileBuilder.append(STRING_CONSTANT_NEW_LINE).append(String.format(STRING_3_PLACEHOLDER_BLOCK, LOCATION_WITH_FULL_HIERARCHY_DESCRIPTION, NEW_LOCATION_NAME_DESCRIPTION, PARENT_LOCATION_WITH_FULL_HIERARCHY_DESCRIPTION)); } sampleFileBuilder.append(String.format(STRING_PLACEHOLDER_BLOCK, GPS_COORDINATES_EXAMPLE)); sampleFileBuilder.append(formElementNamesForLocationTypeFormElements.stream() - .map(fe -> String.format(STRING_PLACEHOLDER_BLOCK, ALLOWED_VALUES +conceptService.getSampleValuesForSyncConcept(fe.getConcept()))) + .map(fe -> String.format(STRING_PLACEHOLDER_BLOCK, ALLOWED_VALUES + conceptService.getSampleValuesForSyncConcept(fe.getConcept()))) .collect(Collectors.joining(STRING_CONSTANT_EMPTY_STRING))); } @@ -235,7 +231,7 @@ private void appendExamplesForLocations(StringBuilder sampleFileBuilder, Locatio } } - private String getUsersAndCatchmentsSampleFile() { + private String getUsersAndCatchmentsSampleFile() { StringBuilder sampleFileBuilder = new StringBuilder(); try (InputStream csvFileResourceStream = this.getClass().getResourceAsStream("/bulkuploads/sample/usersAndCatchments.csv")) { @@ -261,7 +257,7 @@ private void appendDescriptionForUsersAndCatchments(StringBuilder sampleFileBuil .map(Group::getName) .collect(Collectors.joining(", ", "{", "}"))); descriptionRow = allowedValuesForSubjectTypesWithSyncAttributes.isEmpty() ? descriptionRow - : String.format("%s,%s", descriptionRow ,syncAttributesSampleValues); + : String.format("%s,%s", descriptionRow, syncAttributesSampleValues); sampleFileBuilder.append(STRING_CONSTANT_NEW_LINE).append(descriptionRow); } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java new file mode 100644 index 000000000..6c6b9fc9c --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java @@ -0,0 +1,63 @@ +package org.avni.server.importer.batch.csv.writer; + +import org.avni.server.common.AbstractControllerIntegrationTest; +import org.avni.server.importer.batch.model.Row; +import org.avni.server.service.ImportLocationsConstants; +import org.avni.server.service.ImportService; +import org.avni.server.service.LocationHierarchyService; +import org.avni.server.service.builder.TestDataSetupService; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +public class LocationWriterIntegrationTest extends AbstractControllerIntegrationTest { + @Autowired + private TestDataSetupService testDataSetupService; + @Autowired + private LocationWriter locationWriter; + @Autowired + private ImportService importService; + @Autowired + private LocationHierarchyService locationHierarchyService; + + @Test + @Ignore + public void shouldCreate() throws IOException { + TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation(); + TestDataSetupService.TestCatchmentData catchmentData = testDataSetupService.setupACatchment(); + setUser(organisationData.getUser().getUsername()); + + String locationHierarchy = getLocationHierarchy(); + String[] headers = getHeader(locationHierarchy); + +// Row row = new Row(); + locationWriter.write(new ArrayList<>()); + locationWriter.write(new ArrayList<>()); + } + + private String[] getHeader(String locationHierarchy) throws IOException { + String locationsSampleFile = importService.getLocationsSampleFile(LocationWriter.LocationUploadMode.CREATE, locationHierarchy); + BufferedReader bufferedReader = new BufferedReader(new StringReader(locationsSampleFile)); + return new String[]{bufferedReader.readLine(), bufferedReader.readLine()}; + } + + private String getLocationHierarchy() { + HashMap locationHierarchyNames = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); + Set> entries = locationHierarchyNames.entrySet(); + for (Map.Entry entry : entries) { + return entry.getKey(); + } + return null; + } +} diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java index 742effd87..b2f8f6779 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java @@ -10,6 +10,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.mail.Address; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + @Service public class TestDataSetupService { @Autowired @@ -79,6 +85,17 @@ public TestCatchmentData setupACatchment() { return new TestCatchmentData(addressLevelType, addressLevel1, addressLevel2, catchment); } + public AddressLevel setupLocationHierarchy(Map locationTypes, Map locations) { + List addressLevelTypes = locationTypes.entrySet().stream().map(levelNameEntry -> new AddressLevelTypeBuilder().name(levelNameEntry.getValue()).level(levelNameEntry.getKey().doubleValue()).build()).collect(Collectors.toList()); + List savedAddressLevelTypes = addressLevelTypeRepository.saveAll(addressLevelTypes); + + locations.forEach((levelName, locationName) -> { + AddressLevelType addressLevelType = savedAddressLevelTypes.stream().filter(type -> type.getName().equals(levelName)).findFirst().get(); + AddressLevel addressLevel = testLocationService.save(new AddressLevelBuilder().type(addressLevelType).title(locationName).build()); + }); + return null; + } + public TestSyncAttributeBasedSubjectTypeData setupSubjectTypeWithSyncAttributes() { Concept conceptForAttributeBasedSync = testConceptService.createCodedConcept("Concept Name 1", "Answer 1", "Answer 2"); SubjectType subjectType = testSubjectTypeService.createWithDefaults( @@ -91,6 +108,24 @@ public TestSyncAttributeBasedSubjectTypeData setupSubjectTypeWithSyncAttributes( return new TestSyncAttributeBasedSubjectTypeData(subjectType, conceptForAttributeBasedSync); } + public static class TestLocation { + private final String title; + private final String parentTitle; + + public TestLocation(String title, String parentTitle) { + this.title = title; + this.parentTitle = parentTitle; + } + + public String getTitle() { + return title; + } + + public String getParentTitle() { + return parentTitle; + } + } + public static class TestSyncAttributeBasedSubjectTypeData { private final SubjectType subjectType; private final Concept conceptForAttributeBasedSync; From ac2a0077e63a0413d1d5a8741dfa7b1284d3515a Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 19 Aug 2024 14:07:46 +0530 Subject: [PATCH 096/129] #764 - implemented filter bundle upload which was not working correctly. bug fix. --- .../dashboard/DashboardFilterMapper.java | 54 +++++++++++-------- .../mapper/dashboard/DashboardMapper.java | 11 ++-- .../avni/server/service/DashboardService.java | 10 ++-- .../DashboardFilterConfigContract.java | 11 ++-- .../reports/DashboardBundleContract.java | 8 ++- .../DashboardFilterBundleContract.java | 17 ++++++ .../DashboardFilterConfigBundleContract.java | 29 ++++++++++ .../reports/DashboardFilterContract.java | 5 +- .../ObservationBasedFilterContract.java | 10 ++-- .../web/request/DashboardFilterRequest.java | 6 +++ .../web/request/DashboardFilterResponse.java | 7 +++ .../reports/DashboardFilterConfigRequest.java | 10 +++- .../ObservationBasedFilterRequest.java | 7 ++- .../DashboardFilterConfigResponse.java | 6 ++- .../ObservationBasedFilterBundleContract.java | 21 ++++++++ .../ObservationBasedFilterResponse.java | 5 -- 16 files changed, 160 insertions(+), 57 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterBundleContract.java create mode 100644 avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterConfigBundleContract.java create mode 100644 avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterBundleContract.java diff --git a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardFilterMapper.java b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardFilterMapper.java index 786d9363b..d9ee1ff64 100644 --- a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardFilterMapper.java +++ b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardFilterMapper.java @@ -4,6 +4,9 @@ import org.avni.server.domain.Concept; import org.avni.server.domain.app.dashboard.DashboardFilter; import org.avni.server.web.contract.DashboardFilterConfigContract; +import org.avni.server.web.contract.reports.DashboardFilterBundleContract; +import org.avni.server.web.contract.reports.DashboardFilterContract; +import org.avni.server.web.contract.reports.ObservationBasedFilterContract; import org.avni.server.web.request.ConceptContract; import org.avni.server.web.request.DashboardFilterResponse; import org.avni.server.web.response.reports.DashboardFilterConfigResponse; @@ -18,42 +21,47 @@ public DashboardFilterMapper(ConceptRepository conceptRepository) { this.conceptRepository = conceptRepository; } - public DashboardFilterResponse fromEntity(DashboardFilter df) { - DashboardFilterResponse dashboardFilterResponse = new DashboardFilterResponse(); - dashboardFilterResponse.setId(df.getId()); - dashboardFilterResponse.setUuid(df.getUuid()); - dashboardFilterResponse.setVoided(df.isVoided()); - dashboardFilterResponse.setName(df.getName()); - - DashboardFilterConfigResponse filterConfigResponse = new DashboardFilterConfigResponse(); - DashboardFilter.DashboardFilterConfig filterConfig = df.getFilterConfig(); - filterConfigResponse.setSubjectTypeUUID(filterConfig.getSubjectTypeUuid()); - filterConfigResponse.setWidget(filterConfig.getWidget()); - filterConfigResponse.setType(filterConfig.getType().name()); - dashboardFilterResponse.setFilterConfig(filterConfigResponse); - - DashboardFilter.FilterType filterType = df.getFilterConfig().getType(); + public DashboardFilterBundleContract toBundle(DashboardFilter dashboardFilter) { + return (DashboardFilterBundleContract) toContract(dashboardFilter, new DashboardFilterBundleContract()); + } + + private DashboardFilterContract toContract(DashboardFilter dashboardFilter, DashboardFilterContract filterContract) { + filterContract.setId(dashboardFilter.getId()); + filterContract.setUuid(dashboardFilter.getUuid()); + filterContract.setVoided(dashboardFilter.isVoided()); + filterContract.setName(dashboardFilter.getName()); + + DashboardFilterConfigContract filterConfigContract = filterContract.newFilterConfig(); + DashboardFilter.DashboardFilterConfig filterConfig = dashboardFilter.getFilterConfig(); + filterConfigContract.setSubjectTypeUUID(filterConfig.getSubjectTypeUuid()); + filterConfigContract.setWidget(filterConfig.getWidget()); + filterConfigContract.setType(filterConfig.getType().name()); + + DashboardFilter.FilterType filterType = dashboardFilter.getFilterConfig().getType(); if (filterType.equals(DashboardFilter.FilterType.GroupSubject)) { DashboardFilter.GroupSubjectTypeFilter groupSubjectTypeFilter = filterConfig.getGroupSubjectTypeFilter(); DashboardFilterConfigContract.GroupSubjectTypeFilterContract groupSubjectTypeFilterContract = new DashboardFilterConfigContract.GroupSubjectTypeFilterContract(); groupSubjectTypeFilterContract.setSubjectTypeUUID(groupSubjectTypeFilter.getSubjectTypeUUID()); - filterConfigResponse.setGroupSubjectTypeFilter(groupSubjectTypeFilterContract); + filterConfigContract.setGroupSubjectTypeFilter(groupSubjectTypeFilterContract); } else if (filterType.equals(DashboardFilter.FilterType.Concept)) { DashboardFilter.ObservationBasedFilter observationBasedFilter = filterConfig.getObservationBasedFilter(); - ObservationBasedFilterResponse observationBasedFilterResponse = new ObservationBasedFilterResponse(); + ObservationBasedFilterContract observationBasedFilterContract = filterConfigContract.newObservationBasedFilter(); Concept concept = conceptRepository.findByUuid(observationBasedFilter.getConcept()); ConceptContract conceptContract = new ConceptContract(); conceptContract.setUuid(concept.getUuid()); conceptContract.setName(concept.getName()); conceptContract.setDataType(concept.getDataType()); - observationBasedFilterResponse.setConcept(conceptContract); + observationBasedFilterContract.setConcept(conceptContract); - observationBasedFilterResponse.setScope(observationBasedFilter.getScope()); - observationBasedFilterResponse.setProgramUUIDs(observationBasedFilter.getPrograms()); - observationBasedFilterResponse.setEncounterTypeUUIDs(observationBasedFilter.getEncounterTypes()); - filterConfigResponse.setObservationBasedFilter(observationBasedFilterResponse); + observationBasedFilterContract.setScope(observationBasedFilter.getScope()); + observationBasedFilterContract.setProgramUUIDs(observationBasedFilter.getPrograms()); + observationBasedFilterContract.setEncounterTypeUUIDs(observationBasedFilter.getEncounterTypes()); } - return dashboardFilterResponse; + return filterContract; + } + + public DashboardFilterResponse toResponse(DashboardFilter dashboardFilter) { + return (DashboardFilterResponse) toContract(dashboardFilter, new DashboardFilterResponse()); } } diff --git a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardMapper.java b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardMapper.java index 291f25921..0cb0fce01 100644 --- a/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardMapper.java +++ b/avni-server-api/src/main/java/org/avni/server/mapper/dashboard/DashboardMapper.java @@ -3,6 +3,7 @@ import org.avni.server.domain.Dashboard; import org.avni.server.domain.DashboardSection; import org.avni.server.web.contract.reports.DashboardBundleContract; +import org.avni.server.web.contract.reports.DashboardFilterBundleContract; import org.avni.server.web.contract.reports.DashboardSectionBundleContract; import org.avni.server.web.contract.reports.DashboardSectionCardMappingBundleContract; import org.avni.server.web.request.*; @@ -55,17 +56,17 @@ private void setFilters(DashboardWebResponse dashboardContract, Dashboard dashbo List list = dashboard.getDashboardFilters() .stream() .filter(dashboardFilter -> !dashboardFilter.isVoided()) - .map(dashboardFilterMapper::fromEntity) + .map(dashboardFilterMapper::toResponse) .collect(Collectors.toList()); dashboardContract.setFilters(list); } - private void setFilters(DashboardBundleContract dashboardContract, Dashboard dashboard) { - List list = dashboard.getDashboardFilters() + private void setFilters(DashboardBundleContract dashboardBundleContract, Dashboard dashboard) { + List list = dashboard.getDashboardFilters() .stream() - .map(dashboardFilterMapper::fromEntity) + .map(dashboardFilterMapper::toBundle) .collect(Collectors.toList()); - dashboardContract.setFilters(list); + dashboardBundleContract.setFilters(list); } private DashboardSectionWebResponse toWebResponse(DashboardSection ds) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java index 80d633c0c..abdf16134 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java @@ -6,9 +6,7 @@ import org.avni.server.domain.app.dashboard.DashboardFilter; import org.avni.server.util.BadRequestError; import org.avni.server.util.ReactAdminUtil; -import org.avni.server.web.contract.reports.DashboardBundleContract; -import org.avni.server.web.contract.reports.DashboardSectionBundleContract; -import org.avni.server.web.contract.reports.DashboardSectionCardMappingBundleContract; +import org.avni.server.web.contract.reports.*; import org.avni.server.web.request.DashboardFilterRequest; import org.avni.server.web.request.DashboardFilterResponse; import org.avni.server.web.request.DashboardWebRequest; @@ -64,15 +62,15 @@ public void uploadDashboard(DashboardBundleContract dashboardContract) { } private void uploadDashboardFilters(DashboardBundleContract bundleContract, Dashboard dashboard) { - List filters = bundleContract.getFilters(); - for (DashboardFilterResponse bundleFilter : filters) { + List filters = bundleContract.getFilters(); + for (DashboardFilterBundleContract bundleFilter : filters) { DashboardFilter dashboardFilter = dashboardFilterRepository.findByUuid(bundleFilter.getUuid()); if (dashboardFilter == null) { dashboardFilter = new DashboardFilter(); dashboardFilter.setUuid(bundleFilter.getUuid()); } dashboardFilter.setName(bundleFilter.getName()); - DashboardFilterConfigResponse bundleFilterConfig = bundleFilter.getFilterConfig(); + DashboardFilterConfigBundleContract bundleFilterConfig = bundleFilter.getFilterConfig(); dashboardFilter.setFilterConfig(bundleFilterConfig.toJsonObject()); dashboard.addUpdateFilter(dashboardFilter); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java index b9443301e..bb19270de 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/DashboardFilterConfigContract.java @@ -1,7 +1,9 @@ package org.avni.server.web.contract; +import com.fasterxml.jackson.annotation.JsonIgnore; import org.avni.server.domain.JsonObject; import org.avni.server.domain.app.dashboard.DashboardFilter; +import org.avni.server.web.contract.reports.ObservationBasedFilterContract; public abstract class DashboardFilterConfigContract { private String type; @@ -52,12 +54,15 @@ public void setSubjectTypeUUID(String subjectTypeUUID) { this.subjectTypeUUID = subjectTypeUUID; } + @JsonIgnore public JsonObject getJsonObject() { return new JsonObject().with(DashboardFilter.DashboardFilterConfig.SubjectTypeFieldName, subjectTypeUUID); } } - public JsonObject toJsonObject() { + public abstract ObservationBasedFilterContract newObservationBasedFilter(); + + protected JsonObject toJsonObject(JsonObject observationBasedFilter) { JsonObject jsonObject = new JsonObject(); DashboardFilter.FilterType filterType = DashboardFilter.FilterType.valueOf(this.getType()); jsonObject.with(DashboardFilter.DashboardFilterConfig.TypeFieldName, this.getType()) @@ -66,9 +71,7 @@ public JsonObject toJsonObject() { if (filterType.equals(DashboardFilter.FilterType.GroupSubject)) jsonObject.put(DashboardFilter.DashboardFilterConfig.GroupSubjectTypeFilterName, getGroupSubjectTypeFilter().getJsonObject()); else if (filterType.equals(DashboardFilter.FilterType.Concept)) - jsonObject.put(DashboardFilter.DashboardFilterConfig.ObservationBasedFilterName, getObservationTypeFilterJsonObject()); + jsonObject.put(DashboardFilter.DashboardFilterConfig.ObservationBasedFilterName, observationBasedFilter); return jsonObject; } - - protected abstract Object getObservationTypeFilterJsonObject(); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardBundleContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardBundleContract.java index d6e6322da..d885f055b 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardBundleContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardBundleContract.java @@ -1,13 +1,11 @@ package org.avni.server.web.contract.reports; -import org.avni.server.web.request.DashboardFilterResponse; - import java.util.ArrayList; import java.util.List; public class DashboardBundleContract extends DashboardContract { private List sections = new ArrayList<>(); - private List filters = new ArrayList<>(); + private List filters = new ArrayList<>(); public List getSections() { return sections; @@ -17,11 +15,11 @@ public void setSections(List sections) { this.sections = sections; } - public List getFilters() { + public List getFilters() { return filters; } - public void setFilters(List filters) { + public void setFilters(List filters) { this.filters = filters; } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterBundleContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterBundleContract.java new file mode 100644 index 000000000..eed6a1f7e --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterBundleContract.java @@ -0,0 +1,17 @@ +package org.avni.server.web.contract.reports; + +import org.avni.server.web.contract.DashboardFilterConfigContract; + +public class DashboardFilterBundleContract extends DashboardFilterContract { + private DashboardFilterConfigBundleContract filterConfig; + + public DashboardFilterConfigBundleContract getFilterConfig() { + return filterConfig; + } + + @Override + public DashboardFilterConfigContract newFilterConfig() { + this.filterConfig = new DashboardFilterConfigBundleContract(); + return this.filterConfig; + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterConfigBundleContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterConfigBundleContract.java new file mode 100644 index 000000000..44e42d71f --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterConfigBundleContract.java @@ -0,0 +1,29 @@ +package org.avni.server.web.contract.reports; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.avni.server.domain.JsonObject; +import org.avni.server.web.contract.DashboardFilterConfigContract; +import org.avni.server.web.response.reports.ObservationBasedFilterBundleContract; + +public class DashboardFilterConfigBundleContract extends DashboardFilterConfigContract { + private ObservationBasedFilterBundleContract observationBasedFilter; + + public ObservationBasedFilterBundleContract getObservationBasedFilter() { + return observationBasedFilter; + } + + public void setObservationBasedFilter(ObservationBasedFilterBundleContract observationBasedFilter) { + this.observationBasedFilter = observationBasedFilter; + } + + @JsonIgnore + public JsonObject toJsonObject() { + return super.toJsonObject(observationBasedFilter == null ? null : observationBasedFilter.getJsonObject(observationBasedFilter.getConceptUUID())); + } + + @Override + public ObservationBasedFilterContract newObservationBasedFilter() { + this.observationBasedFilter = new ObservationBasedFilterBundleContract(); + return this.observationBasedFilter; + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterContract.java index 83765c7fe..7da621239 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/DashboardFilterContract.java @@ -1,8 +1,9 @@ package org.avni.server.web.contract.reports; +import org.avni.server.web.contract.DashboardFilterConfigContract; import org.avni.server.web.request.CHSRequest; -public class DashboardFilterContract extends CHSRequest { +public abstract class DashboardFilterContract extends CHSRequest { private String name; public String getName() { @@ -12,4 +13,6 @@ public String getName() { public void setName(String name) { this.name = name; } + + public abstract DashboardFilterConfigContract newFilterConfig(); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/ObservationBasedFilterContract.java b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/ObservationBasedFilterContract.java index 1d89ef1e0..fa71944e5 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/contract/reports/ObservationBasedFilterContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/contract/reports/ObservationBasedFilterContract.java @@ -1,7 +1,10 @@ package org.avni.server.web.contract.reports; +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.avni.server.domain.Concept; import org.avni.server.domain.JsonObject; import org.avni.server.domain.app.dashboard.DashboardFilter; +import org.avni.server.web.request.ConceptContract; import java.util.ArrayList; import java.util.List; @@ -35,14 +38,15 @@ public void setScope(String scope) { this.scope = scope; } - public JsonObject getJsonObject() { + @JsonIgnore + public JsonObject getJsonObject(String conceptUUID) { JsonObject jsonObject = new JsonObject(); - jsonObject.put(DashboardFilter.ObservationBasedFilter.ConceptFieldName, this.getConceptUUID()); + jsonObject.put(DashboardFilter.ObservationBasedFilter.ConceptFieldName, conceptUUID); jsonObject.put(DashboardFilter.ObservationBasedFilter.ScopeFieldName, this.getScope()); jsonObject.put(DashboardFilter.ObservationBasedFilter.ProgramsFieldName, getProgramUUIDs()); jsonObject.put(DashboardFilter.ObservationBasedFilter.EncounterTypesFieldName, getEncounterTypeUUIDs()); return jsonObject; } - protected abstract String getConceptUUID(); + public abstract void setConcept(ConceptContract conceptContract); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/DashboardFilterRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/DashboardFilterRequest.java index 122a6c140..90cf60014 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/DashboardFilterRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/DashboardFilterRequest.java @@ -1,5 +1,6 @@ package org.avni.server.web.request; +import org.avni.server.web.contract.DashboardFilterConfigContract; import org.avni.server.web.contract.reports.DashboardFilterContract; import org.avni.server.web.request.reports.DashboardFilterConfigRequest; @@ -13,4 +14,9 @@ public DashboardFilterConfigRequest getFilterConfig() { public void setFilterConfig(DashboardFilterConfigRequest filterConfig) { this.filterConfig = filterConfig; } + + @Override + public DashboardFilterConfigContract newFilterConfig() { + throw new RuntimeException("Not applicable for DashboardFilterRequest, as it constructed via de-serialisation only"); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/DashboardFilterResponse.java b/avni-server-api/src/main/java/org/avni/server/web/request/DashboardFilterResponse.java index 2d905febd..c14e01f5a 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/DashboardFilterResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/DashboardFilterResponse.java @@ -1,5 +1,6 @@ package org.avni.server.web.request; +import org.avni.server.web.contract.DashboardFilterConfigContract; import org.avni.server.web.contract.reports.DashboardFilterContract; import org.avni.server.web.response.reports.DashboardFilterConfigResponse; @@ -13,4 +14,10 @@ public DashboardFilterConfigResponse getFilterConfig() { public void setFilterConfig(DashboardFilterConfigResponse filterConfig) { this.filterConfig = filterConfig; } + + @Override + public DashboardFilterConfigContract newFilterConfig() { + this.filterConfig = new DashboardFilterConfigResponse(); + return this.filterConfig; + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java index 68323b4e2..22559de6d 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/reports/DashboardFilterConfigRequest.java @@ -1,7 +1,9 @@ package org.avni.server.web.request.reports; import com.fasterxml.jackson.annotation.JsonInclude; +import org.avni.server.domain.JsonObject; import org.avni.server.web.contract.DashboardFilterConfigContract; +import org.avni.server.web.contract.reports.ObservationBasedFilterContract; @JsonInclude(JsonInclude.Include.NON_NULL) public class DashboardFilterConfigRequest extends DashboardFilterConfigContract { @@ -15,8 +17,12 @@ public void setObservationBasedFilter(ObservationBasedFilterRequest observationB this.observationBasedFilter = observationBasedFilter; } + public JsonObject toJsonObject() { + return super.toJsonObject(observationBasedFilter == null ? null : observationBasedFilter.getJsonObject(observationBasedFilter.getConceptUUID())); + } + @Override - protected Object getObservationTypeFilterJsonObject() { - return observationBasedFilter.getJsonObject(); + public ObservationBasedFilterContract newObservationBasedFilter() { + throw new RuntimeException("Not applicable for DashboardFilterConfigRequest, as it constructed via de-serialisation only"); } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/reports/ObservationBasedFilterRequest.java b/avni-server-api/src/main/java/org/avni/server/web/request/reports/ObservationBasedFilterRequest.java index 0ca597e75..c268fd913 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/reports/ObservationBasedFilterRequest.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/reports/ObservationBasedFilterRequest.java @@ -1,11 +1,11 @@ package org.avni.server.web.request.reports; import org.avni.server.web.contract.reports.ObservationBasedFilterContract; +import org.avni.server.web.request.ConceptContract; public class ObservationBasedFilterRequest extends ObservationBasedFilterContract { private String conceptUUID; - @Override public String getConceptUUID() { return conceptUUID; } @@ -13,4 +13,9 @@ public String getConceptUUID() { public void setConceptUUID(String conceptUUID) { this.conceptUUID = conceptUUID; } + + @Override + public void setConcept(ConceptContract conceptContract) { + this.conceptUUID = conceptContract.getUuid(); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java b/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java index 10708a847..4ab6c5dd3 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/reports/DashboardFilterConfigResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import org.avni.server.web.contract.DashboardFilterConfigContract; +import org.avni.server.web.contract.reports.ObservationBasedFilterContract; @JsonInclude(JsonInclude.Include.NON_NULL) public class DashboardFilterConfigResponse extends DashboardFilterConfigContract { @@ -16,7 +17,8 @@ public void setObservationBasedFilter(ObservationBasedFilterResponse observation } @Override - protected Object getObservationTypeFilterJsonObject() { - return observationBasedFilter.getJsonObject(); + public ObservationBasedFilterContract newObservationBasedFilter() { + this.observationBasedFilter = new ObservationBasedFilterResponse(); + return this.observationBasedFilter; } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterBundleContract.java b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterBundleContract.java new file mode 100644 index 000000000..db6ed8e77 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterBundleContract.java @@ -0,0 +1,21 @@ +package org.avni.server.web.response.reports; + +import org.avni.server.web.contract.reports.ObservationBasedFilterContract; +import org.avni.server.web.request.ConceptContract; + +public class ObservationBasedFilterBundleContract extends ObservationBasedFilterContract { + private String conceptUUID; + + public String getConceptUUID() { + return conceptUUID; + } + + public void setConceptUUID(String conceptUUID) { + this.conceptUUID = conceptUUID; + } + + @Override + public void setConcept(ConceptContract conceptContract) { + this.conceptUUID = conceptContract.getUuid(); + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterResponse.java b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterResponse.java index b19c09642..a03b1a611 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterResponse.java +++ b/avni-server-api/src/main/java/org/avni/server/web/response/reports/ObservationBasedFilterResponse.java @@ -13,9 +13,4 @@ public ConceptContract getConcept() { public void setConcept(ConceptContract concept) { this.concept = concept; } - - @Override - protected String getConceptUUID() { - return concept.getUuid(); - } } From edf63942515c2920c9d36c7adc14a913a2bcbb8b Mon Sep 17 00:00:00 2001 From: himeshr Date: Mon, 19 Aug 2024 14:52:43 +0530 Subject: [PATCH 097/129] avniproject/avni-webapp#1214 | make headerList modifiable --- .../importer/batch/csv/writer/UserAndCatchmentWriter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index ba4cfa024..decb777bb 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -79,7 +79,7 @@ public void write(List rows) throws Exception { } private void validateHeaders(String[] headers) { - List headerList = Arrays.asList(headers); + List headerList = new ArrayList<>(Arrays.asList(headers)); List allErrorMsgs = new ArrayList<>(); UsersAndCatchmentsHeaders usersAndCatchmentsHeaders = new UsersAndCatchmentsHeaders(); List expectedStandardHeaders = Arrays.asList(usersAndCatchmentsHeaders.getAllHeaders()); From b86347117c76ffa23bbfd0d93e8aeae3c60d4c06 Mon Sep 17 00:00:00 2001 From: himeshr Date: Mon, 19 Aug 2024 15:04:08 +0530 Subject: [PATCH 098/129] avniproject/avni-webapp#1214 | Split errors into separate columns --- .../importer/batch/csv/writer/UserAndCatchmentWriter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index decb777bb..222bde240 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -28,6 +28,7 @@ @Component public class UserAndCatchmentWriter implements ItemWriter, Serializable { + public static final String ERR_MSG_DELIMITER = "\",\""; private final UserService userService; private final CatchmentService catchmentService; private final LocationRepository locationRepository; @@ -87,7 +88,7 @@ private void validateHeaders(String[] headers) { checkForMissingHeaders(headerList, allErrorMsgs, expectedStandardHeaders, syncAttributeHeadersForSubjectTypes); checkForUnknownHeaders(headerList, allErrorMsgs, expectedStandardHeaders, syncAttributeHeadersForSubjectTypes); if(!allErrorMsgs.isEmpty()) { - throw new RuntimeException(String.join(", ", allErrorMsgs)); + throw new RuntimeException(String.join(ERR_MSG_DELIMITER, allErrorMsgs)); } } @@ -179,7 +180,7 @@ private void validateRowAndAssimilateErrors(List rowValidationErrorMsgs, extractUserNameValidationErrMsg(rowValidationErrorMsgs, nameOfUser); extractUserEmailValidationErrMsg(rowValidationErrorMsgs, email); if(!rowValidationErrorMsgs.isEmpty()) { - throw new RuntimeException(String.join(", ", rowValidationErrorMsgs)); + throw new RuntimeException(String.join(ERR_MSG_DELIMITER, rowValidationErrorMsgs)); } } From 9f93ac9b2389c02491cbe35342f153b0b70903b3 Mon Sep 17 00:00:00 2001 From: himeshr Date: Mon, 19 Aug 2024 15:34:40 +0530 Subject: [PATCH 099/129] avniproject/avni-webapp#1214 | Split errors into separate columns --- .../main/java/org/avni/server/service/UserService.java | 6 +++++- .../java/org/avni/server/service/UserServiceTest.java | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserService.java b/avni-server-api/src/main/java/org/avni/server/service/UserService.java index c9800a736..10f009be7 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserService.java @@ -140,7 +140,7 @@ public void addToGroups(User user, String groupsSpecified) { return; } - String[] groupNames = Strings.split(groupsSpecified, ','); + String[] groupNames = splitIntoUserGroupNames(groupsSpecified); Arrays.stream(groupNames).distinct().forEach(groupName -> { if (!StringUtils.hasLength(groupName.trim())) return; @@ -159,6 +159,10 @@ public void addToGroups(User user, String groupsSpecified) { } } + public String[] splitIntoUserGroupNames(String groupsSpecified) { + return groupsSpecified.trim().split("\\s*,\\s*|\\s*\\|\\s*"); + } + public void ensureSubjectsForUserSubjectType(SubjectType subjectType) { List users = userRepository.findAllByIsVoidedFalseAndOrganisationId(subjectType.getOrganisationId()); users.forEach(user -> ensureSubjectForUser(user, subjectType)); diff --git a/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java index 43c2fe597..ed1c1b65d 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/UserServiceTest.java @@ -15,6 +15,7 @@ import java.util.List; import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertArrayEquals; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; @@ -90,4 +91,11 @@ public void shouldDeduplicateListOfGroupsSpecified() { assertEquals(group2, allValues.get(1).getGroup()); assertEquals(everyone, allValues.get(2).getGroup()); } + + @Test + public void testUserGroupSplitByDelimiter() { + final String groupsSpecified = "ug1, ug2|ug3 | ug4-ug5 , ug6 ug7"; + String[] userGroups = userService.splitIntoUserGroupNames(groupsSpecified); + assertArrayEquals(new String[]{"ug1", "ug2", "ug3", "ug4-ug5", "ug6 ug7"}, userGroups); + } } From 2152aa5054f7cc6102ecddd5a5510d1fbe9c6ded Mon Sep 17 00:00:00 2001 From: himeshr Date: Mon, 19 Aug 2024 16:21:19 +0530 Subject: [PATCH 100/129] avniproject/avni-webapp#787 | Grant access to all dbusers for virtual_catchment_address_mapping_table view --- avni-server-api/src/main/resources/db/migration/R__Views.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/avni-server-api/src/main/resources/db/migration/R__Views.sql b/avni-server-api/src/main/resources/db/migration/R__Views.sql index 119bd8bb8..537f6b34e 100644 --- a/avni-server-api/src/main/resources/db/migration/R__Views.sql +++ b/avni-server-api/src/main/resources/db/migration/R__Views.sql @@ -39,6 +39,8 @@ CREATE TRIGGER delete_on_virtual_catchment_address_mapping FOR EACH ROW EXECUTE FUNCTION no_op(); +SELECT grant_all_on_views(ARRAY ['virtual_catchment_address_mapping_table'], a.rolname) FROM pg_roles a WHERE pg_has_role('openchs', a.oid, 'member'); + DROP VIEW if exists address_level_type_view; CREATE VIEW address_level_type_view AS From 330ec595465c266f7fceeb613d4f27195e9a4b8d Mon Sep 17 00:00:00 2001 From: himeshr Date: Mon, 19 Aug 2024 16:54:07 +0530 Subject: [PATCH 101/129] avniproject/avni-webapp#787 | Grant access to all dbusers for all views after recreate --- .../src/main/resources/db/migration/R__Views.sql | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/avni-server-api/src/main/resources/db/migration/R__Views.sql b/avni-server-api/src/main/resources/db/migration/R__Views.sql index 537f6b34e..633c72e8b 100644 --- a/avni-server-api/src/main/resources/db/migration/R__Views.sql +++ b/avni-server-api/src/main/resources/db/migration/R__Views.sql @@ -39,8 +39,6 @@ CREATE TRIGGER delete_on_virtual_catchment_address_mapping FOR EACH ROW EXECUTE FUNCTION no_op(); -SELECT grant_all_on_views(ARRAY ['virtual_catchment_address_mapping_table'], a.rolname) FROM pg_roles a WHERE pg_has_role('openchs', a.oid, 'member'); - DROP VIEW if exists address_level_type_view; CREATE VIEW address_level_type_view AS @@ -102,3 +100,12 @@ FROM (SELECT pe.individual_id, and pe.is_voided = false GROUP BY pe.individual_id, op.name, prog.colour) progralalise GROUP BY progralalise.individual_id; + +SELECT grant_all_on_views( + ARRAY ['address_level_type_view', + 'individual_program_enrolment_search_view', + 'title_lineage_locations_view', + 'virtual_catchment_address_mapping_table'], + a.rolname) +FROM pg_roles a +WHERE pg_has_role('openchs', a.oid, 'member'); \ No newline at end of file From 20bdb04f250b1c2503687e7917cdb32402381521 Mon Sep 17 00:00:00 2001 From: himeshr Date: Tue, 20 Aug 2024 11:22:47 +0530 Subject: [PATCH 102/129] avniproject/avni-webapp#1214 | Move syncConcept validation to before validateRowAndAssimilateErrors --- .../importer/batch/csv/writer/UserAndCatchmentWriter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index 222bde240..32b489e94 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -126,6 +126,7 @@ private void write(Row row) throws Exception { Locale locale = S.isEmpty(language) ? Locale.en : Locale.valueByName(language); Organisation organisation = UserContextHolder.getUserContext().getOrganisation(); String userSuffix = "@".concat(organisation.getEffectiveUsernameSuffix()); + JsonObject syncSettings = constructSyncSettings(row, rowValidationErrorMsgs); validateRowAndAssimilateErrors(rowValidationErrorMsgs, fullAddress, catchmentName, nameOfUser, username, email, phoneNumber, language, datePickerMode, location, locale, userSuffix); Catchment catchment = catchmentService.createOrUpdate(catchmentName, location); User user = userRepository.findByUsername(username.trim()); @@ -140,7 +141,6 @@ private void write(Row row) throws Exception { user.setEmail(email); userService.setPhoneNumber(phoneNumber, user, RegionUtil.getCurrentUserRegion()); user.setName(nameOfUser.trim()); - JsonObject syncSettings = constructSyncSettings(row, rowValidationErrorMsgs); if (!isNewUser) resetSyncService.recordSyncAttributeValueChangeForUser(user, catchment.getId(), syncSettings); user.setCatchment(catchment); user.setOperatingIndividualScope(ByCatchment); From 108d778a15e8982092ce257b20e742d5557f4156 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 20 Aug 2024 12:00:45 +0530 Subject: [PATCH 103/129] #764 - more builders for test --- .../batch/csv/LocationImportService.java | 8 +++ .../batch/csv/writer/LocationWriter.java | 3 +- .../domain/factory/AddressLevelBuilder.java | 5 ++ .../factory/AddressLevelTypeBuilder.java | 5 ++ .../writer/LocationWriterIntegrationTest.java | 56 +++++++----------- .../service/builder/TestDataSetupService.java | 58 +++++++++++++------ 6 files changed, 79 insertions(+), 56 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/importer/batch/csv/LocationImportService.java diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/LocationImportService.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/LocationImportService.java new file mode 100644 index 000000000..ad2485f5c --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/LocationImportService.java @@ -0,0 +1,8 @@ +package org.avni.server.importer.batch.csv; + +import org.springframework.stereotype.Component; + +@Component +public class LocationImportService { + +} diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java index 88391d181..ddde67ab4 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java @@ -28,15 +28,14 @@ import java.util.*; import java.util.stream.Collectors; - @StepScope @Component public class LocationWriter implements ItemWriter { - private static final LocationHeaders headers = new LocationHeaders(); @Value("#{jobParameters['locationUploadMode']}") private String locationUploadMode; @Value("#{jobParameters['locationHierarchy']}") private String locationHierarchy; + private static final LocationHeaders headers = new LocationHeaders(); private final LocationService locationService; private final LocationRepository locationRepository; private final AddressLevelTypeRepository addressLevelTypeRepository; diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java index c7c6f246e..b5cb4e55c 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java @@ -37,6 +37,11 @@ public AddressLevelBuilder id(long id) { return this; } + public AddressLevelBuilder withUuid(UUID uuid) { + entity.setUuid(uuid.toString()); + return this; + } + public AddressLevelBuilder withUuid(String uuid) { entity.setUuid(uuid); return this; diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelTypeBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelTypeBuilder.java index 5b2465369..ca2181ce2 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelTypeBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelTypeBuilder.java @@ -17,6 +17,11 @@ public AddressLevelTypeBuilder level(Double level) { return this; } + public AddressLevelTypeBuilder withUuid(UUID uuid) { + addressLevelType.setUuid(uuid.toString()); + return this; + } + public AddressLevelTypeBuilder withUuid(String uuid) { addressLevelType.setUuid(uuid); return this; diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java index 6c6b9fc9c..f9c4d1dc0 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java @@ -2,22 +2,14 @@ import org.avni.server.common.AbstractControllerIntegrationTest; import org.avni.server.importer.batch.model.Row; -import org.avni.server.service.ImportLocationsConstants; -import org.avni.server.service.ImportService; -import org.avni.server.service.LocationHierarchyService; import org.avni.server.service.builder.TestDataSetupService; import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; -import java.util.Map; -import java.util.Set; @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @@ -26,38 +18,32 @@ public class LocationWriterIntegrationTest extends AbstractControllerIntegration private TestDataSetupService testDataSetupService; @Autowired private LocationWriter locationWriter; - @Autowired - private ImportService importService; - @Autowired - private LocationHierarchyService locationHierarchyService; @Test @Ignore - public void shouldCreate() throws IOException { + public void shouldCreate() { TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation(); - TestDataSetupService.TestCatchmentData catchmentData = testDataSetupService.setupACatchment(); - setUser(organisationData.getUser().getUsername()); - - String locationHierarchy = getLocationHierarchy(); - String[] headers = getHeader(locationHierarchy); -// Row row = new Row(); - locationWriter.write(new ArrayList<>()); - locationWriter.write(new ArrayList<>()); - } - - private String[] getHeader(String locationHierarchy) throws IOException { - String locationsSampleFile = importService.getLocationsSampleFile(LocationWriter.LocationUploadMode.CREATE, locationHierarchy); - BufferedReader bufferedReader = new BufferedReader(new StringReader(locationsSampleFile)); - return new String[]{bufferedReader.readLine(), bufferedReader.readLine()}; + testDataSetupService.setupLocationHierarchy( + new HashMap() { + { + put(4, "State"); + put(3, "District"); + put(2, "Block"); + } + }, + new HashMap() {{ + put("State", "Bihar"); + put("District", "Vaishali"); + put("Block", "Mahua"); + }} + ); + setUser(organisationData.getUser().getUsername()); + String[] validHeaders = new String[]{"State", "District", "Block", "GPS coordinates"}; + success(validHeaders, "Bihar", "Vaishali", "Aiana", "23.45,43.85"); } - private String getLocationHierarchy() { - HashMap locationHierarchyNames = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); - Set> entries = locationHierarchyNames.entrySet(); - for (Map.Entry entry : entries) { - return entry.getKey(); - } - return null; + private void success(String[] headers, String... cells) { + locationWriter.write(Collections.singletonList(new Row(headers, cells))); } } diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java index b2f8f6779..776a384e0 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java @@ -11,9 +11,7 @@ import org.springframework.stereotype.Service; import javax.mail.Address; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Service @@ -85,15 +83,29 @@ public TestCatchmentData setupACatchment() { return new TestCatchmentData(addressLevelType, addressLevel1, addressLevel2, catchment); } - public AddressLevel setupLocationHierarchy(Map locationTypes, Map locations) { - List addressLevelTypes = locationTypes.entrySet().stream().map(levelNameEntry -> new AddressLevelTypeBuilder().name(levelNameEntry.getValue()).level(levelNameEntry.getKey().doubleValue()).build()).collect(Collectors.toList()); - List savedAddressLevelTypes = addressLevelTypeRepository.saveAll(addressLevelTypes); + public TestLocationHierarchyData setupLocationHierarchy(Map locationTypes, Map locations) { + Map namedLocationTypes = new HashMap<>(); + List addressLevelTypes = locationTypes.entrySet().stream().map(levelNameEntry -> + new AddressLevelTypeBuilder() + .name(levelNameEntry.getValue()) + .level(levelNameEntry.getKey().doubleValue()) + .withUuid(UUID.randomUUID()) + .build()).collect(Collectors.toList()); + for (AddressLevelType addressLevelType : addressLevelTypes) { + AddressLevelType type = addressLevelTypeRepository.save(addressLevelType); + namedLocationTypes.put(addressLevelType.getName(), type); + } + HashMap namedLocations = new HashMap<>(); locations.forEach((levelName, locationName) -> { - AddressLevelType addressLevelType = savedAddressLevelTypes.stream().filter(type -> type.getName().equals(levelName)).findFirst().get(); - AddressLevel addressLevel = testLocationService.save(new AddressLevelBuilder().type(addressLevelType).title(locationName).build()); + AddressLevelType addressLevelType = namedLocationTypes.get(levelName); + namedLocations.put(levelName, testLocationService.save(new AddressLevelBuilder() + .type(addressLevelType) + .title(locationName) + .withUuid(UUID.randomUUID()) + .build())); }); - return null; + return new TestLocationHierarchyData(namedLocationTypes, namedLocations); } public TestSyncAttributeBasedSubjectTypeData setupSubjectTypeWithSyncAttributes() { @@ -108,21 +120,29 @@ public TestSyncAttributeBasedSubjectTypeData setupSubjectTypeWithSyncAttributes( return new TestSyncAttributeBasedSubjectTypeData(subjectType, conceptForAttributeBasedSync); } - public static class TestLocation { - private final String title; - private final String parentTitle; + public static class TestLocationHierarchyData { + private final Map locationTypes; + private final Map locations; + + public TestLocationHierarchyData(Map locationTypes, Map locations) { + this.locationTypes = locationTypes; + this.locations = locations; + } + + public Map getLocationTypes() { + return locationTypes; + } - public TestLocation(String title, String parentTitle) { - this.title = title; - this.parentTitle = parentTitle; + public Map getLocations() { + return locations; } - public String getTitle() { - return title; + public AddressLevelType getLocationType(String name) { + return locationTypes.get(name); } - public String getParentTitle() { - return parentTitle; + public AddressLevel getLocation(String name) { + return locations.get(name); } } From 0daf3878e54fa17a546f9c07390d0d4b306c2296 Mon Sep 17 00:00:00 2001 From: himeshr Date: Tue, 20 Aug 2024 12:46:38 +0530 Subject: [PATCH 104/129] #750 | Add check to skip org specific config for new super-admin user creation --- .../java/org/avni/server/service/UserService.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserService.java b/avni-server-api/src/main/java/org/avni/server/service/UserService.java index 10f009be7..4e4d47fbb 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserService.java @@ -52,7 +52,7 @@ public User getCurrentUser() { public User save(User user) { String idPrefix = UserSettings.getIdPrefix(user.getSettings()); - if (StringUtils.hasLength(idPrefix)) { + if (user.getOrganisationId() != null && StringUtils.hasLength(idPrefix) ) { // Not a super-admin and has idPrefix synchronized (String.format("%d-USER-ID-PREFIX-%s", user.getOrganisationId(), idPrefix).intern()) { List usersWithSameIdPrefix = user.isNew() ? userRepository.getAllUsersWithSameIdPrefix(idPrefix) : userRepository.getUsersWithSameIdPrefix(idPrefix, user.getId()); if (usersWithSameIdPrefix.isEmpty()) { @@ -67,16 +67,19 @@ public User save(User user) { } private User createUpdateUser(User user) { - SubjectType userSubjectType = subjectTypeRepository.findByTypeAndIsVoidedFalse(Subject.User); User savedUser = userRepository.save(user); - if (userSubjectType != null) - this.ensureSubjectForUser(user, userSubjectType); + if (user.getOrganisationId() != null) { // Not a super-admin + SubjectType userSubjectType = subjectTypeRepository.findByTypeAndIsVoidedFalse(Subject.User); + if (userSubjectType != null) { + this.ensureSubjectForUser(user, userSubjectType); + } + } return savedUser; } @Transactional public void addToDefaultUserGroup(User user) { - if (user.getOrganisationId() != null) { + if (user.getOrganisationId() != null) { //Not a super-admin Group group = groupRepository.findByNameAndOrganisationId(Group.Everyone, user.getOrganisationId()); if (userGroupRepository.findByUserAndGroupAndIsVoidedFalse(user, group) == null) { UserGroup userGroup = UserGroup.createMembership(user, group); From 8639402ef73d10065a159b2f71805d1518510289 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 20 Aug 2024 13:21:55 +0530 Subject: [PATCH 105/129] avniproject/avni-product#1646 - transform phone number before updating --- .../src/main/java/org/avni/server/web/UserController.java | 4 +--- .../main/java/org/avni/server/web/UserInfoController.java | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserController.java b/avni-server-api/src/main/java/org/avni/server/web/UserController.java index 5b0664cd7..8ae90cf82 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserController.java @@ -195,9 +195,7 @@ private User setUserAttributes(User user, UserContract userContract, String user throw new ValidationException(String.format("Invalid email address %s", userContract.getEmail())); user.setEmail(userContract.getEmail()); - if (!phoneNumberIsValid(userContract.getPhoneNumber(), userRegion)) - throw new ValidationException(String.format("Invalid phone number %s", userContract.getPhoneNumber())); - user.setPhoneNumber(userContract.getPhoneNumber()); + userService.setPhoneNumber(userContract.getPhoneNumber(), user, userRegion); if (isUserNameInvalid(userContract.getUsername())) { throw new ValidationException(String.format("Invalid username %s", userContract.getUsername())); diff --git a/avni-server-api/src/main/java/org/avni/server/web/UserInfoController.java b/avni-server-api/src/main/java/org/avni/server/web/UserInfoController.java index aa876f1c3..3171bb86e 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/UserInfoController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/UserInfoController.java @@ -10,6 +10,8 @@ import org.avni.server.service.*; import org.avni.server.service.accessControl.AccessControlService; import org.avni.server.service.accessControl.GroupPrivilegeService; +import org.avni.server.util.PhoneNumberUtil; +import org.avni.server.util.RegionUtil; import org.avni.server.web.request.GroupPrivilegeContract; import org.avni.server.web.request.UserBulkUploadContract; import org.avni.server.web.request.UserInfoClientContract; @@ -159,8 +161,7 @@ public void save(@RequestBody UserBulkUploadContract[] userContracts) { user.setOrganisationId(organisationId); user.setOperatingIndividualScope(OperatingIndividualScope.valueOf(userContract.getOperatingIndividualScope())); user.setSettings(userContract.getSettings()); - user.setPhoneNumber(userContract.getPhoneNumber()); - user.setEmail(userContract.getEmail()); + userService.setPhoneNumber(userContract.getPhoneNumber(), user, RegionUtil.getCurrentUserRegion()); user.setAuditInfo(userService.getCurrentUser()); User savedUser = userService.save(user); if (newUser) userService.addToDefaultUserGroup(savedUser); From 0350404db5f39a40c708214089935a6e3c82aa2f Mon Sep 17 00:00:00 2001 From: Joy A Date: Tue, 20 Aug 2024 14:59:57 +0530 Subject: [PATCH 106/129] avniproject/avni-product#1646 | Restore changes for #749 after merge --- .../org/avni/server/service/OrganisationService.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index d8ff6df32..f02569432 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -940,8 +940,9 @@ private void createGender(String genderName, Organisation org) { } private Group addDefaultGroupIfNotPresent(Long organisationId, String groupType) { - if (Objects.nonNull(groupRepository.findByNameAndOrganisationId(groupType, organisationId))) { - return null; + Group existingGroup = groupRepository.findByNameAndOrganisationId(groupType, organisationId); + if (Objects.nonNull(existingGroup)) { + return existingGroup; } Group group = new Group(); group.setName(groupType); @@ -964,8 +965,10 @@ public void setupBaseOrganisationAdminConfig(Organisation organisation) { public void setupBaseOrganisationMetadata(Organisation organisation) { createDefaultGenders(organisation); - addDefaultGroupIfNotPresent(organisation.getId(), Group.Everyone); + Group everyoneGroup = addDefaultGroupIfNotPresent(organisation.getId(), Group.Everyone); addDefaultGroupIfNotPresent(organisation.getId(), Group.Administrators); + Dashboard defaultDashboard = dashboardService.createDefaultDashboard(organisation); + groupDashboardService.createDefaultGroupDashboardForOrg(organisation, everyoneGroup, defaultDashboard); } public Organisation getCurrentOrganisation() { From c7ecc36bfe25fef82d415b4d9ac8cb1a14bf5317 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 21 Aug 2024 13:19:54 +0530 Subject: [PATCH 107/129] avniproject/avni-webapp#1306 - LocationWriter broken into creator and editor. Each can be integration tested now. --- .../batch/csv/writer/BulkLocationCreator.java | 124 ++++++++++++ .../batch/csv/writer/BulkLocationEditor.java | 78 ++++++++ .../csv/writer/BulkLocationModifier.java | 28 +++ .../batch/csv/writer/LocationWriter.java | 187 +----------------- .../csv/writer/ProgramEnrolmentWriter.java | 25 ++- .../batch/csv/writer/SubjectWriter.java | 10 +- ...Test.java => BulkLocationCreatorTest.java} | 27 +-- 7 files changed, 261 insertions(+), 218 deletions(-) create mode 100644 avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java create mode 100644 avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java create mode 100644 avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java rename avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/{LocationWriterTest.java => BulkLocationCreatorTest.java} (76%) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java new file mode 100644 index 000000000..7fcb5f5fe --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java @@ -0,0 +1,124 @@ +package org.avni.server.importer.batch.csv.writer; + +import org.avni.server.application.FormElement; +import org.avni.server.builder.BuilderException; +import org.avni.server.dao.AddressLevelTypeRepository; +import org.avni.server.dao.LocationRepository; +import org.avni.server.domain.AddressLevel; +import org.avni.server.domain.AddressLevelType; +import org.avni.server.importer.batch.csv.creator.ObservationCreator; +import org.avni.server.importer.batch.csv.writer.header.LocationHeaders; +import org.avni.server.importer.batch.model.Row; +import org.avni.server.service.FormService; +import org.avni.server.service.ImportService; +import org.avni.server.service.LocationService; +import org.avni.server.util.S; +import org.avni.server.web.request.LocationContract; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +// This class is need so that the logic can be instantiated in integration tests. Spring batch configuration is not working in integration tests. +@Component +public class BulkLocationCreator extends BulkLocationModifier { + private final LocationService locationService; + private final LocationRepository locationRepository; + private final AddressLevelTypeRepository addressLevelTypeRepository; + private final ImportService importService; + private final FormService formService; + + public BulkLocationCreator(LocationService locationService, LocationRepository locationRepository, AddressLevelTypeRepository addressLevelTypeRepository, ObservationCreator observationCreator, ImportService importService, FormService formService) { + super(locationRepository, observationCreator); + this.locationService = locationService; + this.locationRepository = locationRepository; + this.addressLevelTypeRepository = addressLevelTypeRepository; + this.importService = importService; + this.formService = formService; + } + + public List getLocationTypeNames() { + List locationTypes = addressLevelTypeRepository.findAllByIsVoidedFalse(); + locationTypes.sort(Comparator.comparingDouble(AddressLevelType::getLevel).reversed()); + return locationTypes.stream().map(AddressLevelType::getName).collect(Collectors.toList()); + } + + public void createLocation(Row row, List allErrorMsgs, List locationTypeNames) { + AddressLevel parent = null; + AddressLevel location = null; + for (String header : row.getHeaders()) { + if (isValidLocation(header, row, locationTypeNames)) { + location = createAddressLevel(row, parent, header, locationTypeNames); + parent = location; + } //This will get called only when location have extra properties + if (location != null && !locationTypeNames.contains(header)) { + updateLocationProperties(row, allErrorMsgs, location); + } + } + } + + private List validateCreateModeHeaders(String[] headers, List allErrorMsgs, String locationHierarchy) { + List headerList = Arrays.asList(headers); + List locationTypeHeaders = checkIfHeaderHasLocationTypesInOrderForHierarchy(locationHierarchy, headerList, allErrorMsgs); + List additionalHeaders = new ArrayList<>(headerList.subList(locationTypeHeaders.size(), headerList.size())); + checkIfHeaderRowHasUnknownHeaders(additionalHeaders, allErrorMsgs); + return locationTypeHeaders; + } + + private List checkIfHeaderHasLocationTypesInOrderForHierarchy(String locationHierarchy, List headerList, List allErrorMsgs) { + List locationTypeNamesForHierachy = importService.getAddressLevelTypesForCreateModeSingleHierarchy(locationHierarchy) + .stream().map(AddressLevelType::getName).collect(Collectors.toList()); + + if (headerList.size() >= locationTypeNamesForHierachy.size() && !headerList.subList(0, locationTypeNamesForHierachy.size()).equals(locationTypeNamesForHierachy)) { + allErrorMsgs.add("Location types missing or not in order in header for specified Location Hierarchy. Please refer to sample file for valid list of headers."); + throw new RuntimeException(String.join(", ", allErrorMsgs)); + } + return locationTypeNamesForHierachy; + } + + private void checkIfHeaderRowHasUnknownHeaders(List additionalHeaders, List allErrorMsgs) { + additionalHeaders.removeIf(StringUtils::isEmpty); + if (!additionalHeaders.isEmpty()) { + List locationPropertyNames = formService.getFormElementNamesForLocationTypeForms() + .stream().map(FormElement::getName).collect(Collectors.toList()); + locationPropertyNames.add(LocationHeaders.gpsCoordinates); + if ((!locationPropertyNames.containsAll(additionalHeaders))) { + allErrorMsgs.add("Unknown headers included in file. Please refer to sample file for valid list of headers."); + throw new RuntimeException(String.join(", ", allErrorMsgs)); + } + } + } + + private AddressLevel createAddressLevel(Row row, AddressLevel parent, String header, List locationTypeNames) throws BuilderException { + AddressLevel location; + location = locationRepository.findChildLocation(parent, row.get(header)); + if (location == null) { + LocationContract locationContract = new LocationContract(); + locationContract.setupUuidIfNeeded(); + locationContract.setName(row.get(header)); + locationContract.setType(header); + locationContract.setLevel(parent == null ? locationTypeNames.size() : parent.getLevel() - 1); + if (parent != null) { + locationContract.setParent(new LocationContract(parent.getUuid())); + } + location = locationService.save(locationContract); + } + return location; + } + + private boolean isValidLocation(String header, Row row, List locationTypeNames) { + return locationTypeNames.contains(header) && !S.isEmpty(row.get(header)); + } + + public void write(List rows, String locationHierarchy) { + List allErrorMsgs = new ArrayList<>(); + List locationTypeNames = validateCreateModeHeaders(rows.get(0).getHeaders(), allErrorMsgs, locationHierarchy); + for (Row row : rows) { + createLocation(row, allErrorMsgs, locationTypeNames); + } + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java new file mode 100644 index 000000000..0d04f5651 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java @@ -0,0 +1,78 @@ +package org.avni.server.importer.batch.csv.writer; + +import org.avni.server.dao.LocationRepository; +import org.avni.server.domain.AddressLevel; +import org.avni.server.importer.batch.csv.creator.ObservationCreator; +import org.avni.server.importer.batch.model.Row; +import org.avni.server.service.ImportLocationsConstants; +import org.avni.server.service.LocationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +@Component +public class BulkLocationEditor extends BulkLocationModifier { + private final LocationService locationService; + + @Autowired + public BulkLocationEditor(LocationRepository locationRepository, ObservationCreator observationCreator, LocationService locationService) { + super(locationRepository, observationCreator); + this.locationService = locationService; + } + + public void editLocation(Row row, List allErrorMsgs) { + String existingLocationTitleLineage = row.get(ImportLocationsConstants.COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY); + if (existingLocationTitleLineage.equalsIgnoreCase(ImportLocationsConstants.LOCATION_WITH_FULL_HIERARCHY_DESCRIPTION)) return; + String newLocationParentTitleLineage = row.get(ImportLocationsConstants.COLUMN_NAME_PARENT_LOCATION_WITH_FULL_HIERARCHY); + + Optional existingLocationAddressLevel = locationRepository.findByTitleLineageIgnoreCase(existingLocationTitleLineage); + if (!existingLocationAddressLevel.isPresent()) { + allErrorMsgs.add(String.format("Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'", existingLocationTitleLineage)); + throw new RuntimeException(String.join(", ", allErrorMsgs)); + } + + AddressLevel newLocationParentAddressLevel = null; + if (!StringUtils.isEmpty(newLocationParentTitleLineage)) { + newLocationParentAddressLevel = locationRepository.findByTitleLineageIgnoreCase(newLocationParentTitleLineage).orElse(null); + if (newLocationParentAddressLevel == null) { + allErrorMsgs.add(String.format("Provided new Location parent does not exist in Avni. Please add it or check for spelling mistakes '%s'", newLocationParentTitleLineage)); + } + } + updateExistingLocation(existingLocationAddressLevel.get(), newLocationParentAddressLevel, row, allErrorMsgs); + } + + public void validateEditModeHeaders(String[] headers, List allErrorMsgs) { + List headerList = Arrays.asList(headers); + if (!headerList.contains(ImportLocationsConstants.COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY)) { + allErrorMsgs.add(String.format("'%s' is required", ImportLocationsConstants.COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY)); + } + if (!(headerList.contains(ImportLocationsConstants.COLUMN_NAME_NEW_LOCATION_NAME) + || headerList.contains(ImportLocationsConstants.COLUMN_NAME_GPS_COORDINATES) + || headerList.contains(ImportLocationsConstants.COLUMN_NAME_PARENT_LOCATION_WITH_FULL_HIERARCHY))) { + allErrorMsgs.add(String.format("At least one of '%s', '%s' or '%s' is required", ImportLocationsConstants.COLUMN_NAME_NEW_LOCATION_NAME, ImportLocationsConstants.COLUMN_NAME_GPS_COORDINATES, ImportLocationsConstants.COLUMN_NAME_PARENT_LOCATION_WITH_FULL_HIERARCHY)); + } + if (!allErrorMsgs.isEmpty()) { + throw new RuntimeException(String.join(", ", allErrorMsgs)); + } + } + + private void updateExistingLocation(AddressLevel location, AddressLevel newParent, Row row, List allErrorMsgs) { + String newTitle = row.get(ImportLocationsConstants.COLUMN_NAME_NEW_LOCATION_NAME); + if (!StringUtils.isEmpty(newTitle)) location.setTitle(newTitle); + if (newParent != null) { + locationService.updateParent(location, newParent); + } + updateLocationProperties(row, allErrorMsgs, location); + } + + public void write(List rows, List allErrorMsgs) { + validateEditModeHeaders(rows.get(0).getHeaders(), allErrorMsgs); + for (Row row : rows) { + editLocation(row, allErrorMsgs); + } + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java new file mode 100644 index 000000000..8ace97568 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java @@ -0,0 +1,28 @@ +package org.avni.server.importer.batch.csv.writer; + +import org.avni.server.application.FormType; +import org.avni.server.dao.LocationRepository; +import org.avni.server.domain.AddressLevel; +import org.avni.server.importer.batch.csv.creator.ObservationCreator; +import org.avni.server.importer.batch.csv.writer.header.LocationHeaders; +import org.avni.server.importer.batch.model.Row; + +import java.util.List; + +public abstract class BulkLocationModifier { + protected final LocationRepository locationRepository; + private final ObservationCreator observationCreator; + protected static final LocationHeaders headers = new LocationHeaders(); + + public BulkLocationModifier(LocationRepository locationRepository, ObservationCreator observationCreator) { + this.locationRepository = locationRepository; + this.observationCreator = observationCreator; + } + + protected void updateLocationProperties(Row row, List allErrorMsgs, AddressLevel location) { + org.avni.server.importer.batch.csv.creator.LocationCreator locationCreator = new org.avni.server.importer.batch.csv.creator.LocationCreator(); + location.setGpsCoordinates(locationCreator.getLocation(row, LocationHeaders.gpsCoordinates, allErrorMsgs)); + location.setLocationProperties(observationCreator.getObservations(row, headers, allErrorMsgs, FormType.Location, location.getLocationProperties())); + locationRepository.save(location); + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java index ddde67ab4..71778e7a3 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java @@ -1,214 +1,45 @@ package org.avni.server.importer.batch.csv.writer; -import org.avni.server.application.FormElement; -import org.avni.server.application.FormType; -import org.avni.server.builder.BuilderException; -import org.avni.server.dao.AddressLevelTypeRepository; -import org.avni.server.dao.LocationRepository; -import org.avni.server.domain.AddressLevel; -import org.avni.server.domain.AddressLevelType; -import org.avni.server.importer.batch.csv.creator.LocationCreator; -import org.avni.server.importer.batch.csv.creator.ObservationCreator; -import org.avni.server.importer.batch.csv.writer.header.LocationHeaders; import org.avni.server.importer.batch.model.Row; -import org.avni.server.service.FormService; -import org.avni.server.service.ImportLocationsConstants; -import org.avni.server.service.ImportService; -import org.avni.server.service.LocationService; -import org.avni.server.util.S; -import org.avni.server.web.request.LocationContract; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.ItemWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import javax.annotation.PostConstruct; import java.util.*; -import java.util.stream.Collectors; @StepScope @Component public class LocationWriter implements ItemWriter { + private final BulkLocationCreator bulkLocationCreator; + private final BulkLocationEditor bulkLocationEditor; @Value("#{jobParameters['locationUploadMode']}") private String locationUploadMode; @Value("#{jobParameters['locationHierarchy']}") private String locationHierarchy; - private static final LocationHeaders headers = new LocationHeaders(); - private final LocationService locationService; - private final LocationRepository locationRepository; - private final AddressLevelTypeRepository addressLevelTypeRepository; - private final LocationCreator locationCreator; - private final ObservationCreator observationCreator; - private final ImportService importService; - private final FormService formService; - private List locationTypeNames; + private List editLocationTypeNames; @Autowired - public LocationWriter(LocationService locationService, - LocationRepository locationRepository, - AddressLevelTypeRepository addressLevelTypeRepository, - ObservationCreator observationCreator, - ImportService importService, - FormService formService) { - this.locationService = locationService; - this.locationRepository = locationRepository; - this.addressLevelTypeRepository = addressLevelTypeRepository; - this.observationCreator = observationCreator; - this.importService = importService; - this.formService = formService; - this.locationCreator = new LocationCreator(); + public LocationWriter(BulkLocationCreator bulkLocationCreator, BulkLocationEditor bulkLocationEditor) { + this.bulkLocationCreator = bulkLocationCreator; + this.bulkLocationEditor = bulkLocationEditor; } @PostConstruct public void init() { - List locationTypes = addressLevelTypeRepository.findAllByIsVoidedFalse(); - locationTypes.sort(Comparator.comparingDouble(AddressLevelType::getLevel).reversed()); - this.locationTypeNames = locationTypes.stream().map(AddressLevelType::getName).collect(Collectors.toList()); + this.editLocationTypeNames = this.bulkLocationCreator.getLocationTypeNames(); } @Override public void write(List rows) { List allErrorMsgs = new ArrayList<>(); if (LocationUploadMode.isCreateMode(locationUploadMode)) { - validateCreateModeHeaders(rows.get(0).getHeaders(), allErrorMsgs); + this.bulkLocationCreator.write(rows, locationHierarchy); } else { - validateEditModeHeaders(rows.get(0).getHeaders(), allErrorMsgs); + this.bulkLocationEditor.write(rows, allErrorMsgs); } - for (Row row : rows) { - if (LocationUploadMode.isCreateMode(locationUploadMode)) { - createLocation(row, allErrorMsgs); - } else { - editLocation(row, allErrorMsgs); - } - } - } - - private void editLocation(Row row, List allErrorMsgs) { - String existingLocationTitleLineage = row.get(ImportLocationsConstants.COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY); - if (existingLocationTitleLineage.equalsIgnoreCase(ImportLocationsConstants.LOCATION_WITH_FULL_HIERARCHY_DESCRIPTION)) return; - String newLocationParentTitleLineage = row.get(ImportLocationsConstants.COLUMN_NAME_PARENT_LOCATION_WITH_FULL_HIERARCHY); - - Optional existingLocationAddressLevel = locationRepository.findByTitleLineageIgnoreCase(existingLocationTitleLineage); - if (!existingLocationAddressLevel.isPresent()) { - allErrorMsgs.add(String.format("Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'", existingLocationTitleLineage)); - throw new RuntimeException(String.join(", ", allErrorMsgs)); - } - - AddressLevel newLocationParentAddressLevel = null; - if (!StringUtils.isEmpty(newLocationParentTitleLineage)) { - newLocationParentAddressLevel = locationRepository.findByTitleLineageIgnoreCase(newLocationParentTitleLineage).orElse(null); - if (newLocationParentAddressLevel == null) { - allErrorMsgs.add(String.format("Provided new Location parent does not exist in Avni. Please add it or check for spelling mistakes '%s'", newLocationParentTitleLineage)); - } - } - updateExistingLocation(existingLocationAddressLevel.get(), newLocationParentAddressLevel, row, allErrorMsgs); - } - - private void createLocation(Row row, List allErrorMsgs) { - AddressLevel parent = null; - AddressLevel location = null; - for (String header : row.getHeaders()) { - if (isValidLocation(header, row, this.locationTypeNames)) { - location = createAddressLevel(row, parent, header); - parent = location; - } //This will get called only when location have extra properties - if (location != null && !this.locationTypeNames.contains(header)) { - updateLocationProperties(row, allErrorMsgs, location); - } - } - } - - private void updateLocationProperties(Row row, List allErrorMsgs, AddressLevel location) { - location.setGpsCoordinates(locationCreator.getLocation(row, LocationHeaders.gpsCoordinates, allErrorMsgs)); - location.setLocationProperties(observationCreator.getObservations(row, headers, allErrorMsgs, FormType.Location, location.getLocationProperties())); - locationRepository.save(location); - } - - private void validateCreateModeHeaders(String[] headers, List allErrorMsgs) { - List headerList = Arrays.asList(headers); - List locationTypeHeaders = checkIfHeaderHasLocationTypesInOrderForHierarchy(this.locationHierarchy, headerList, allErrorMsgs); - List additionalHeaders = new ArrayList<>(headerList.subList(locationTypeHeaders.size(), headerList.size())); - checkIfHeaderRowHasUnknownHeaders(additionalHeaders, allErrorMsgs); - } - - private void validateEditModeHeaders(String[] headers, List allErrorMsgs) { - List headerList = Arrays.asList(headers); - if (!headerList.contains(ImportLocationsConstants.COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY)) { - allErrorMsgs.add(String.format("'%s' is required", ImportLocationsConstants.COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY)); - } - if (!(headerList.contains(ImportLocationsConstants.COLUMN_NAME_NEW_LOCATION_NAME) - || headerList.contains(ImportLocationsConstants.COLUMN_NAME_GPS_COORDINATES) - || headerList.contains(ImportLocationsConstants.COLUMN_NAME_PARENT_LOCATION_WITH_FULL_HIERARCHY))) { - allErrorMsgs.add(String.format("At least one of '%s', '%s' or '%s' is required", ImportLocationsConstants.COLUMN_NAME_NEW_LOCATION_NAME, ImportLocationsConstants.COLUMN_NAME_GPS_COORDINATES, ImportLocationsConstants.COLUMN_NAME_PARENT_LOCATION_WITH_FULL_HIERARCHY)); - } - if (!allErrorMsgs.isEmpty()) { - throw new RuntimeException(String.join(", ", allErrorMsgs)); - } - } - - private List checkIfHeaderHasLocationTypesInOrderForHierarchy(String locationHierarchy, List headerList, List allErrorMsgs) { - List locationTypeNamesForHierachy = importService.getAddressLevelTypesForCreateModeSingleHierarchy(locationHierarchy) - .stream().map(AddressLevelType::getName).collect(Collectors.toList()); - this.locationTypeNames = locationTypeNamesForHierachy; - - if (headerList.size() >= locationTypeNamesForHierachy.size() && !headerList.subList(0, locationTypeNamesForHierachy.size()).equals(locationTypeNamesForHierachy)) { - allErrorMsgs.add("Location types missing or not in order in header for specified Location Hierarchy. Please refer to sample file for valid list of headers."); - throw new RuntimeException(String.join(", ", allErrorMsgs)); - } - return locationTypeNamesForHierachy; - } - - private void checkIfHeaderRowHasUnknownHeaders(List additionalHeaders, List allErrorMsgs) { - additionalHeaders.removeIf(StringUtils::isEmpty); - if (!additionalHeaders.isEmpty()) { - List locationPropertyNames = formService.getFormElementNamesForLocationTypeForms() - .stream().map(FormElement::getName).collect(Collectors.toList()); - locationPropertyNames.add(LocationHeaders.gpsCoordinates); - if ((!locationPropertyNames.containsAll(additionalHeaders))) { - allErrorMsgs.add("Unknown headers included in file. Please refer to sample file for valid list of headers."); - throw new RuntimeException(String.join(", ", allErrorMsgs)); - } - } - } - - private AddressLevel createAddressLevel(Row row, AddressLevel parent, String header) throws BuilderException { - AddressLevel location; - location = locationRepository.findChildLocation(parent, row.get(header)); - if (location == null) { - LocationContract locationContract = new LocationContract(); - locationContract.setupUuidIfNeeded(); - locationContract.setName(row.get(header)); - locationContract.setType(header); - locationContract.setLevel(parent == null ? this.locationTypeNames.size() : parent.getLevel() - 1); - if (parent != null) { - locationContract.setParent(new LocationContract(parent.getUuid())); - } - location = locationService.save(locationContract); - } - return location; - } - - private boolean isValidLocation(String header, Row row, List locationTypeNames) { - return locationTypeNames.contains(header) && !S.isEmpty(row.get(header)); - } - - private void updateExistingLocation(AddressLevel location, AddressLevel newParent, Row row, List allErrorMsgs) { - String newTitle = row.get(ImportLocationsConstants.COLUMN_NAME_NEW_LOCATION_NAME); - if (!StringUtils.isEmpty(newTitle)) location.setTitle(newTitle); - if (newParent != null) { - locationService.updateParent(location, newParent); - } - updateLocationProperties(row, allErrorMsgs, location); - } - - public void setLocationUploadMode(String locationUploadMode) { - this.locationUploadMode = locationUploadMode; - } - - public void setLocationHierarchy(String locationHierarchy) { - this.locationHierarchy = locationHierarchy; } public enum LocationUploadMode { diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/ProgramEnrolmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/ProgramEnrolmentWriter.java index f45d12226..aaca43832 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/ProgramEnrolmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/ProgramEnrolmentWriter.java @@ -28,18 +28,17 @@ @Component public class ProgramEnrolmentWriter extends EntityWriter implements ItemWriter, Serializable { private final ProgramEnrolmentRepository programEnrolmentRepository; - private LocationCreator locationCreator; - private SubjectCreator subjectCreator; - private DateCreator dateCreator; - private ProgramCreator programCreator; - private FormMappingRepository formMappingRepository; - private ObservationService observationService; - private RuleServerInvoker ruleServerInvoker; - private VisitCreator visitCreator; - private DecisionCreator decisionCreator; - private ObservationCreator observationCreator; - private ProgramEnrolmentService programEnrolmentService; - private EntityApprovalStatusWriter entityApprovalStatusWriter; + private final SubjectCreator subjectCreator; + private final DateCreator dateCreator; + private final ProgramCreator programCreator; + private final FormMappingRepository formMappingRepository; + private final ObservationService observationService; + private final RuleServerInvoker ruleServerInvoker; + private final VisitCreator visitCreator; + private final DecisionCreator decisionCreator; + private final ObservationCreator observationCreator; + private final ProgramEnrolmentService programEnrolmentService; + private final EntityApprovalStatusWriter entityApprovalStatusWriter; @Autowired public ProgramEnrolmentWriter(ProgramEnrolmentRepository programEnrolmentRepository, @@ -66,7 +65,6 @@ public ProgramEnrolmentWriter(ProgramEnrolmentRepository programEnrolmentReposit this.observationCreator = observationCreator; this.programEnrolmentService = programEnrolmentService; this.entityApprovalStatusWriter = entityApprovalStatusWriter; - this.locationCreator = new LocationCreator(); this.dateCreator = new DateCreator(); } @@ -95,6 +93,7 @@ private void write(Row row) throws Exception { ); if (exitDate != null) programEnrolment.setProgramExitDateTime(exitDate.toDateTimeAtStartOfDay()); + LocationCreator locationCreator = new LocationCreator(); programEnrolment.setEnrolmentLocation(locationCreator.getLocation(row, ProgramEnrolmentHeaders.enrolmentLocation, allErrorMsgs)); programEnrolment.setExitLocation(locationCreator.getLocation(row, ProgramEnrolmentHeaders.exitLocation, allErrorMsgs)); programEnrolment.setProgram(program); diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/SubjectWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/SubjectWriter.java index ba028bac7..a509f2292 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/SubjectWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/SubjectWriter.java @@ -3,10 +3,8 @@ import org.avni.server.application.FormMapping; import org.avni.server.application.FormType; import org.avni.server.application.Subject; -import org.avni.server.dao.AddressLevelTypeRepository; import org.avni.server.dao.GenderRepository; import org.avni.server.dao.IndividualRepository; -import org.avni.server.dao.LocationRepository; import org.avni.server.dao.application.FormMappingRepository; import org.avni.server.domain.*; import org.avni.server.importer.batch.csv.contract.UploadRuleServerResponseContract; @@ -26,13 +24,11 @@ import java.util.ArrayList; import java.util.List; - @Component public class SubjectWriter extends EntityWriter implements ItemWriter, Serializable { private final IndividualRepository individualRepository; private final GenderRepository genderRepository; private final SubjectTypeCreator subjectTypeCreator; - private final LocationCreator locationCreator; private final FormMappingRepository formMappingRepository; private final ObservationService observationService; private final RuleServerInvoker ruleServerInvoker; @@ -49,9 +45,7 @@ public class SubjectWriter extends EntityWriter implements ItemWriter, Seri private static final Logger logger = LoggerFactory.getLogger(SubjectWriter.class); @Autowired - public SubjectWriter(AddressLevelTypeRepository addressLevelTypeRepository, - LocationRepository locationRepository, - IndividualRepository individualRepository, + public SubjectWriter(IndividualRepository individualRepository, GenderRepository genderRepository, SubjectTypeCreator subjectTypeCreator, FormMappingRepository formMappingRepository, @@ -78,7 +72,6 @@ public SubjectWriter(AddressLevelTypeRepository addressLevelTypeRepository, this.addressLevelCreator = addressLevelCreator; this.subjectMigrationService = subjectMigrationService; this.subjectTypeService = subjectTypeService; - this.locationCreator = new LocationCreator(); this.s3Service = s3Service; } @@ -104,6 +97,7 @@ private void write(Row row) throws Exception { setDateOfBirth(individual, row, allErrorMsgs); individual.setDateOfBirthVerified(row.getBool(SubjectHeaders.dobVerified)); setRegistrationDate(individual, row, allErrorMsgs); + LocationCreator locationCreator = new LocationCreator(); individual.setRegistrationLocation(locationCreator.getLocation(row, SubjectHeaders.registrationLocation, allErrorMsgs)); AddressLevelTypes registrationLocationTypes = subjectTypeService.getRegistrableLocationTypes(subjectType); diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorTest.java similarity index 76% rename from avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterTest.java rename to avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorTest.java index 5b8a36eed..2bd5e5ad5 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorTest.java @@ -20,9 +20,10 @@ import static org.mockito.Mockito.*; -public class LocationWriterTest { +public class BulkLocationCreatorTest { private ImportService importService; - private LocationWriter locationWriter; + private BulkLocationCreator bulkLocationCreator; + private String dummyHierarchy; @Before public void setup() { @@ -30,8 +31,9 @@ public void setup() { LocationRepository locationRepository = mock(LocationRepository.class); LocationService locationService = mock(LocationService.class); importService = mock(ImportService.class); - locationWriter = new LocationWriter(locationService, locationRepository, addressLevelTypeRepository, mock(ObservationCreator.class), importService, mock(FormService.class)); + bulkLocationCreator = new BulkLocationCreator(locationService, locationRepository, addressLevelTypeRepository, mock(ObservationCreator.class), importService, mock(FormService.class)); when(locationService.save(any())).thenReturn(new AddressLevel()); + dummyHierarchy = "1.2"; } @Test(expected = Exception.class) @@ -45,7 +47,7 @@ public void shouldDisallowUnknownHeadersInCreateFile() throws Exception { rows.add(new Row(headers, new String[]{b1.getTitle(), v1.getTitle(), "new address level"})); - locationWriter.write(rows); + bulkLocationCreator.write(rows, dummyHierarchy); } @Test(expected = Exception.class) @@ -58,14 +60,10 @@ public void shouldIncludeHierarchyInOrder() throws Exception { rows.add(new Row(headers, new String[]{b1.getTitle(), v1.getTitle()})); - locationWriter.write(rows); + bulkLocationCreator.write(rows, dummyHierarchy); } - private void createModeFileSetup() throws Exception { - String dummyHierarchy = "1.2"; - locationWriter.setLocationUploadMode(String.valueOf(LocationWriter.LocationUploadMode.CREATE)); - locationWriter.setLocationHierarchy(dummyHierarchy); - + private void createModeFileSetup() { when(importService.getAddressLevelTypesForCreateModeSingleHierarchy(dummyHierarchy)).thenReturn(hierarchyOneOfAddressLevelTypes()); } @@ -77,13 +75,4 @@ private List hierarchyOneOfAddressLevelTypes() { private AddressLevelType blockType() { return new AddressLevelTypeBuilder().name("Block").level(3.0).build(); } - - private List hierarchyTwoOfAddressLevelTypes() { - return Arrays.asList(subcenter(), - new AddressLevelTypeBuilder().name("AWC").level(2.0).build()); - } - - private AddressLevelType subcenter() { - return new AddressLevelTypeBuilder().name("Sub Center").level(3.0).build(); - } } From e8848d5afc8336aecb0cf0182d218b8b0d7c234b Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 21 Aug 2024 14:00:11 +0530 Subject: [PATCH 108/129] #776 | Setup base org metadata as well during org creation --- .../java/org/avni/server/service/OrganisationService.java | 5 +++++ .../java/org/avni/server/web/OrganisationController.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index e0e1eb49b..9b45163d0 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -944,6 +944,11 @@ private void createDefaultGenders(Organisation org) { createGender("Other", org); } + public void setupBaseOrganisationData(Organisation organisation) { + setupBaseOrganisationMetadata(organisation); + setupBaseOrganisationAdminConfig(organisation); + } + public void setupBaseOrganisationAdminConfig(Organisation organisation) { organisationConfigService.createDefaultOrganisationConfig(organisation); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java index bd68c0ed3..c6fc8a43e 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/OrganisationController.java @@ -60,7 +60,7 @@ public ResponseEntity save(@RequestBody OrganisationContract request) { setOrgAccountByIdOrDefault(org, request.getAccountId()); organisationRepository.save(org); - organisationService.setupBaseOrganisationAdminConfig(org); + organisationService.setupBaseOrganisationData(org); return new ResponseEntity<>(org, HttpStatus.CREATED); } From 2ec78cf384a29eb8616acc37a02b2245f225b7b8 Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 21 Aug 2024 14:42:41 +0530 Subject: [PATCH 109/129] #776 | Perform Genders existence check specific to org --- .../src/main/java/org/avni/server/dao/GenderRepository.java | 1 + .../main/java/org/avni/server/service/OrganisationService.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/GenderRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/GenderRepository.java index e4da3b7dd..a875043a7 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/GenderRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/GenderRepository.java @@ -9,6 +9,7 @@ @RepositoryRestResource(collectionResourceRel = "gender", path = "gender") public interface GenderRepository extends CHSRepository, CustomCHSJpaRepository, FindByLastModifiedDateTime { Gender findByName(String name); + Gender findByNameAndOrganisationId(String name, Long organisationId); Gender findByNameIgnoreCase(String name); @RestResource(exported = false) diff --git a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java index 9b45163d0..538ffc34e 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/OrganisationService.java @@ -915,7 +915,7 @@ public void addGroupDashboardJson(ZipOutputStream zos) throws IOException { } private void createGender(String genderName, Organisation org) { - if (Objects.nonNull(genderRepository.findByName(genderName))) { + if (Objects.nonNull(genderRepository.findByNameAndOrganisationId(genderName, org.getId()))) { return; } Gender gender = new Gender(); From dbd13a6dcb8fff1672358ebb203e616d12f21b2a Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 21 Aug 2024 16:50:24 +0530 Subject: [PATCH 110/129] avniproject/avni-webapp#1306 - logging enabled for unit tests. changed log statement logging error with only message. first location creator test working. --- Makefile | 2 + avni-server-api/build.gradle | 1 + .../org/avni/server/domain/AddressLevel.java | 2 +- .../avni/server/domain/AddressLevelType.java | 2 +- .../server/domain/ParentLocationMapping.java | 2 - .../batch/csv/writer/BulkLocationEditor.java | 4 +- .../csv/writer/BulkLocationModifier.java | 3 +- .../batch/csv/writer/LocationWriter.java | 9 +--- .../service/LocationHierarchyService.java | 2 +- .../avni/server/service/LocationService.java | 4 +- .../org/avni/server/util/WebResponseUtil.java | 2 +- .../avni/server/web/ExtensionController.java | 8 +-- .../org/avni/server/web/FormController.java | 2 +- .../org/avni/server/web/ImportController.java | 2 +- .../avni/server/web/LocationController.java | 4 +- .../org/avni/server/web/MediaController.java | 8 +-- .../OrganisationConfigSearchController.java | 2 +- .../src/main/resources/log4j.properties | 9 ++++ .../BuildLocationCreatorIntegrationTest.java | 47 ++++++++++++++++++ .../writer/LocationWriterIntegrationTest.java | 49 ------------------- .../service/builder/TestDataSetupService.java | 30 +++--------- .../src/test/resources/tear-down.sql | 2 + 22 files changed, 93 insertions(+), 103 deletions(-) create mode 100644 avni-server-api/src/main/resources/log4j.properties create mode 100644 avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java delete mode 100644 avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java diff --git a/Makefile b/Makefile index 286e916ff..0770eb4fc 100644 --- a/Makefile +++ b/Makefile @@ -133,12 +133,14 @@ debug_server_remote_db: build_server build_server: ## Builds the jar file ./gradlew clean build -x test +build-server: build_server boot_run: OPENCHS_DATABASE=$(DB) ./gradlew bootRun test_server: rebuild_testdb ## Run tests GRADLE_OPTS="-Xmx256m" ./gradlew clean test +test-server: test_server test_server_without_clean_rebuild: ## Run tests GRADLE_OPTS="-Xmx256m" ./gradlew test diff --git a/avni-server-api/build.gradle b/avni-server-api/build.gradle index d88016a4f..fd6f03335 100644 --- a/avni-server-api/build.gradle +++ b/avni-server-api/build.gradle @@ -90,6 +90,7 @@ dependencies { compile 'org.owasp.encoder:encoder:1.2.3' implementation 'org.apache.commons:commons-csv:1.10.0' compile 'com.googlecode.libphonenumber:libphonenumber:8.12.32' + testImplementation 'org.slf4j:slf4j-reload4j:2.0.6' } bootRun { diff --git a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java index b3d47a863..7c8056d5c 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevel.java @@ -33,7 +33,7 @@ public class AddressLevel extends OrganisationAwareEntity { @JoinColumn(name = "type_id") private AddressLevelType type; - @ManyToOne(cascade = {CascadeType.ALL}) + @ManyToOne @JoinColumn(name = "parent_id") private AddressLevel parent; diff --git a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevelType.java b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevelType.java index 14b8d5d99..15186678a 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/AddressLevelType.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/AddressLevelType.java @@ -27,7 +27,7 @@ public class AddressLevelType extends OrganisationAwareEntity { @PositiveOrZero private Double level; - @ManyToOne(cascade = {CascadeType.ALL}) + @ManyToOne(cascade = {CascadeType.MERGE}) private AddressLevelType parent; @OneToMany(mappedBy = "parent") diff --git a/avni-server-api/src/main/java/org/avni/server/domain/ParentLocationMapping.java b/avni-server-api/src/main/java/org/avni/server/domain/ParentLocationMapping.java index c9d5f0fca..b0255947b 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/ParentLocationMapping.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/ParentLocationMapping.java @@ -8,12 +8,10 @@ @Table(name = "location_location_mapping") @BatchSize(size = 100) public class ParentLocationMapping extends OrganisationAwareEntity { -// @JsonIgnore @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "location_id") private AddressLevel location; -// @NotNull @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "parent_location_id") private AddressLevel parentLocation; diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java index 0d04f5651..6582ac37c 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -69,7 +70,8 @@ private void updateExistingLocation(AddressLevel location, AddressLevel newParen updateLocationProperties(row, allErrorMsgs, location); } - public void write(List rows, List allErrorMsgs) { + public void write(List rows) { + List allErrorMsgs = new ArrayList<>(); validateEditModeHeaders(rows.get(0).getHeaders(), allErrorMsgs); for (Row row : rows) { editLocation(row, allErrorMsgs); diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java index 8ace97568..20b0f8a63 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java @@ -3,6 +3,7 @@ import org.avni.server.application.FormType; import org.avni.server.dao.LocationRepository; import org.avni.server.domain.AddressLevel; +import org.avni.server.importer.batch.csv.creator.LocationCreator; import org.avni.server.importer.batch.csv.creator.ObservationCreator; import org.avni.server.importer.batch.csv.writer.header.LocationHeaders; import org.avni.server.importer.batch.model.Row; @@ -20,7 +21,7 @@ public BulkLocationModifier(LocationRepository locationRepository, ObservationCr } protected void updateLocationProperties(Row row, List allErrorMsgs, AddressLevel location) { - org.avni.server.importer.batch.csv.creator.LocationCreator locationCreator = new org.avni.server.importer.batch.csv.creator.LocationCreator(); + LocationCreator locationCreator = new LocationCreator(); location.setGpsCoordinates(locationCreator.getLocation(row, LocationHeaders.gpsCoordinates, allErrorMsgs)); location.setLocationProperties(observationCreator.getObservations(row, headers, allErrorMsgs, FormType.Location, location.getLocationProperties())); locationRepository.save(location); diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java index 71778e7a3..ca96e7b50 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/LocationWriter.java @@ -19,7 +19,6 @@ public class LocationWriter implements ItemWriter { private String locationUploadMode; @Value("#{jobParameters['locationHierarchy']}") private String locationHierarchy; - private List editLocationTypeNames; @Autowired public LocationWriter(BulkLocationCreator bulkLocationCreator, BulkLocationEditor bulkLocationEditor) { @@ -27,18 +26,12 @@ public LocationWriter(BulkLocationCreator bulkLocationCreator, BulkLocationEdito this.bulkLocationEditor = bulkLocationEditor; } - @PostConstruct - public void init() { - this.editLocationTypeNames = this.bulkLocationCreator.getLocationTypeNames(); - } - @Override public void write(List rows) { - List allErrorMsgs = new ArrayList<>(); if (LocationUploadMode.isCreateMode(locationUploadMode)) { this.bulkLocationCreator.write(rows, locationHierarchy); } else { - this.bulkLocationEditor.write(rows, allErrorMsgs); + this.bulkLocationEditor.write(rows); } } diff --git a/avni-server-api/src/main/java/org/avni/server/service/LocationHierarchyService.java b/avni-server-api/src/main/java/org/avni/server/service/LocationHierarchyService.java index bfb51aadb..b39290ac6 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/LocationHierarchyService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/LocationHierarchyService.java @@ -71,7 +71,7 @@ public TreeSet determineAddressHierarchiesToBeSaved(JsonObject organisat return filterHierarchiesWithCommonAncestries(addressLevelTypeHierarchies); } - public HashMap determineAddressHierarchiesForAllAddressLevelTypesInOrg() { + public Map determineAddressHierarchiesForAllAddressLevelTypesInOrg() { HashMap hierarchyToDisplayNameMap = new HashMap<>(); List addressLevelTypes = addressLevelTypeRepository.findAllByIsVoidedFalse(); TreeSet addressLevelTypeHierarchies = buildHierarchyForAddressLevelTypes(addressLevelTypes); diff --git a/avni-server-api/src/main/java/org/avni/server/service/LocationService.java b/avni-server-api/src/main/java/org/avni/server/service/LocationService.java index c511a5f55..1e31dd0ea 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/LocationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/LocationService.java @@ -111,14 +111,14 @@ private AddressLevel saveLocation(LocationContract contract, AddressLevelType ty try { locationRepository.save(location); } catch (Exception e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); throw new BuilderException(String.format("Unable to create Location{name='%s',level='%s',orgUUID='%s',..}: '%s' (%s)", contract.getName(), contract.getLevel(), contract.getOrganisationUUID(), e.getMessage(), contract)); } try { location.calculateLineage(); locationRepository.save(location); } catch (Exception e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); throw new BuilderException(String.format("Unable to update lineage for location with Id %s - %s. (%s)", location.getId(), e.getMessage(), contract)); } return location; diff --git a/avni-server-api/src/main/java/org/avni/server/util/WebResponseUtil.java b/avni-server-api/src/main/java/org/avni/server/util/WebResponseUtil.java index d6a528f29..10171d5e4 100644 --- a/avni-server-api/src/main/java/org/avni/server/util/WebResponseUtil.java +++ b/avni-server-api/src/main/java/org/avni/server/util/WebResponseUtil.java @@ -8,7 +8,7 @@ public class WebResponseUtil { public static ResponseEntity> createBadRequestResponse(Exception e, Logger logger) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.badRequest().body(generateJsonError(e.getMessage())); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/ExtensionController.java b/avni-server-api/src/main/java/org/avni/server/web/ExtensionController.java index 3684e778a..ec2f806f0 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ExtensionController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ExtensionController.java @@ -106,10 +106,10 @@ public ResponseEntity serveExtensionFile(@CookieValue(name = "IMPLEMENTATION- InputStream contentStream = s3Service.getExtensionContent(format("%s/%s", OrganisationConfig.Extension.EXTENSION_DIR, filePath), organisation); return ResponseEntity.ok().body(new InputStreamResource(contentStream)); } catch (AccessDeniedException e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorBodyBuilder.getErrorMessageBody(e)); } catch (Exception e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(format("Error in serving file %s", filePath)); } } @@ -131,10 +131,10 @@ public ResponseEntity serveCustomPrintFile(@CookieValue(name = "IMPLEMENTATIO logger.debug(format("S3 signed URL: %s", url.toString())); return ResponseEntity.status(HttpStatus.FOUND).location(url.toURI()).build(); } catch (AccessDeniedException e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorBodyBuilder.getErrorMessageBody(e)); } catch (Exception e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorBodyBuilder.getErrorBody(e)); } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/FormController.java b/avni-server-api/src/main/java/org/avni/server/web/FormController.java index 6525796e5..a34d4baad 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/FormController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/FormController.java @@ -456,7 +456,7 @@ public List getFormIdentifiers(@PathVariable Str .map(identifierAssignment -> projectionFactory.createProjection(IdentifierAssignmentProjection.class, identifierAssignment)) .collect(Collectors.toList()); } catch (Exception e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); throw e; } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/ImportController.java b/avni-server-api/src/main/java/org/avni/server/web/ImportController.java index c53467a4e..a3fb5e562 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ImportController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ImportController.java @@ -190,7 +190,7 @@ public JsonObject getSubjectOrLocationObsValue(@RequestParam("type") String type @GetMapping(value = "/web/locationHierarchies") @ResponseBody - public HashMap getAllAddressLevelTypeHierarchies() { + public Map getAllAddressLevelTypeHierarchies() { try { return locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); } catch (Exception exception) { diff --git a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java index b1629ffbb..4978cc9c6 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java @@ -69,7 +69,7 @@ public ResponseEntity save(@RequestBody List locationContra return new ResponseEntity<>(list.get(0), HttpStatus.CREATED); } } catch (BuilderException e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.badRequest().body(ReactAdminUtil.generateJsonError(e.getMessage())); } return ResponseEntity.ok(null); @@ -127,7 +127,7 @@ public ResponseEntity updateLocation(@RequestBody LocationEditContract locationE try { location = locationService.update(locationEditContract, id); } catch (Exception e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.badRequest().body(ReactAdminUtil.generateJsonError(e.getMessage())); } return new ResponseEntity<>(location, HttpStatus.OK); diff --git a/avni-server-api/src/main/java/org/avni/server/web/MediaController.java b/avni-server-api/src/main/java/org/avni/server/web/MediaController.java index 7b2463e62..0bdc3e668 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/MediaController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/MediaController.java @@ -64,10 +64,10 @@ private ResponseEntity getFileUrlResponse(String fileName, HttpMethod me logger.debug(format("Generating pre-signed url: %s", url.toString())); return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(url.toString()); } catch (AccessDeniedException e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorBodyBuilder.getErrorMessageBody(e)); } catch (Exception e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorBodyBuilder.getErrorBody(e)); } } @@ -108,10 +108,10 @@ public ResponseEntity generateDownloadUrl(@RequestParam String url) { try { return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(s3Service.generateMediaDownloadUrl(url).toString()); } catch (AccessDeniedException e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorBodyBuilder.getErrorMessageBody(e)); } catch (Exception e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorBodyBuilder.getErrorBody(e)); } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/OrganisationConfigSearchController.java b/avni-server-api/src/main/java/org/avni/server/web/OrganisationConfigSearchController.java index 049d77f0a..684566462 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/OrganisationConfigSearchController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/OrganisationConfigSearchController.java @@ -36,7 +36,7 @@ public ResponseEntity> getOrganisationSearchConfig try { return new ResponseEntity<>(organisationConfigService.getOrganisationSettings(organisationId), HttpStatus.OK); } catch (Exception e) { - logger.error(e.getMessage()); + logger.error(e.getMessage(), e); } return ResponseEntity.status(500).build(); } diff --git a/avni-server-api/src/main/resources/log4j.properties b/avni-server-api/src/main/resources/log4j.properties new file mode 100644 index 000000000..065753297 --- /dev/null +++ b/avni-server-api/src/main/resources/log4j.properties @@ -0,0 +1,9 @@ +# Set root logger level to DEBUG and its only appender to A1. +log4j.rootLogger=ERROR, A1 + +# A1 is set to be a ConsoleAppender. +log4j.appender.A1=org.apache.log4j.ConsoleAppender + +# A1 uses PatternLayout. +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=[%c{2}] %-5p - %m%n diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java new file mode 100644 index 000000000..867c26340 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java @@ -0,0 +1,47 @@ +package org.avni.server.importer.batch.csv.writer; + +import org.avni.server.common.AbstractControllerIntegrationTest; +import org.avni.server.dao.AddressLevelTypeRepository; +import org.avni.server.domain.AddressLevelType; +import org.avni.server.domain.factory.AddressLevelTypeBuilder; +import org.avni.server.importer.batch.model.Row; +import org.avni.server.service.LocationHierarchyService; +import org.avni.server.service.builder.TestDataSetupService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +public class BuildLocationCreatorIntegrationTest extends AbstractControllerIntegrationTest { + @Autowired + private TestDataSetupService testDataSetupService; + @Autowired + private BulkLocationCreator bulkLocationCreator; + @Autowired + private LocationHierarchyService locationHierarchyService; + + @Test + public void shouldCreate() { + TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation(); + AddressLevelType block = new AddressLevelTypeBuilder().name("Block").level(2d).withUuid(UUID.randomUUID()).build(); + AddressLevelType district = new AddressLevelTypeBuilder().name("District").level(3d).withUuid(UUID.randomUUID()).build(); + AddressLevelType state = new AddressLevelTypeBuilder().name("State").level(4d).withUuid(UUID.randomUUID()).build(); + testDataSetupService.saveLocationTypes(Arrays.asList(block, district, state)); + + setUser(organisationData.getUser().getUsername()); + Map hierarchies = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); + String hierarchy = String.join(".", hierarchies.keySet()); + String[] validHeaders = new String[]{"State", "District", "Block", "GPS coordinates"}; + success(validHeaders, hierarchy, "Bihar", "Vaishali", "Mahua", "23.45,43.85"); + } + + private void success(String[] headers, String hierarchy, String... cells) { + bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); + } +} diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java deleted file mode 100644 index f9c4d1dc0..000000000 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/LocationWriterIntegrationTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.avni.server.importer.batch.csv.writer; - -import org.avni.server.common.AbstractControllerIntegrationTest; -import org.avni.server.importer.batch.model.Row; -import org.avni.server.service.builder.TestDataSetupService; -import org.junit.Ignore; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.jdbc.Sql; - -import java.util.Collections; -import java.util.HashMap; - -@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) -@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) -public class LocationWriterIntegrationTest extends AbstractControllerIntegrationTest { - @Autowired - private TestDataSetupService testDataSetupService; - @Autowired - private LocationWriter locationWriter; - - @Test - @Ignore - public void shouldCreate() { - TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation(); - - testDataSetupService.setupLocationHierarchy( - new HashMap() { - { - put(4, "State"); - put(3, "District"); - put(2, "Block"); - } - }, - new HashMap() {{ - put("State", "Bihar"); - put("District", "Vaishali"); - put("Block", "Mahua"); - }} - ); - setUser(organisationData.getUser().getUsername()); - String[] validHeaders = new String[]{"State", "District", "Block", "GPS coordinates"}; - success(validHeaders, "Bihar", "Vaishali", "Aiana", "23.45,43.85"); - } - - private void success(String[] headers, String... cells) { - locationWriter.write(Collections.singletonList(new Row(headers, cells))); - } -} diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java index 776a384e0..fa6a18936 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java @@ -83,29 +83,13 @@ public TestCatchmentData setupACatchment() { return new TestCatchmentData(addressLevelType, addressLevel1, addressLevel2, catchment); } - public TestLocationHierarchyData setupLocationHierarchy(Map locationTypes, Map locations) { - Map namedLocationTypes = new HashMap<>(); - List addressLevelTypes = locationTypes.entrySet().stream().map(levelNameEntry -> - new AddressLevelTypeBuilder() - .name(levelNameEntry.getValue()) - .level(levelNameEntry.getKey().doubleValue()) - .withUuid(UUID.randomUUID()) - .build()).collect(Collectors.toList()); - for (AddressLevelType addressLevelType : addressLevelTypes) { - AddressLevelType type = addressLevelTypeRepository.save(addressLevelType); - namedLocationTypes.put(addressLevelType.getName(), type); - } - - HashMap namedLocations = new HashMap<>(); - locations.forEach((levelName, locationName) -> { - AddressLevelType addressLevelType = namedLocationTypes.get(levelName); - namedLocations.put(levelName, testLocationService.save(new AddressLevelBuilder() - .type(addressLevelType) - .title(locationName) - .withUuid(UUID.randomUUID()) - .build())); - }); - return new TestLocationHierarchyData(namedLocationTypes, namedLocations); + public void saveLocationTypes(List lowestToHighestAddressLevelTypes) { + AddressLevelType lastAddressLevelType = null; + for (AddressLevelType addressLevelType : lowestToHighestAddressLevelTypes) { + if (lastAddressLevelType != null) + addressLevelType.addChildAddressLevelType(lastAddressLevelType); + lastAddressLevelType = addressLevelTypeRepository.save(addressLevelType); + } } public TestSyncAttributeBasedSubjectTypeData setupSubjectTypeWithSyncAttributes() { diff --git a/avni-server-api/src/test/resources/tear-down.sql b/avni-server-api/src/test/resources/tear-down.sql index 8375a5392..cbe5c0aa2 100644 --- a/avni-server-api/src/test/resources/tear-down.sql +++ b/avni-server-api/src/test/resources/tear-down.sql @@ -26,6 +26,7 @@ DELETE FROM program where 1 = 1; DELETE FROM encounter_type where 1 = 1; DELETE FROM gender where 1 = 1; DELETE FROM catchment_address_mapping where 1 = 1; +DELETE FROM location_location_mapping where 1 = 1; DELETE FROM address_level where 1 = 1; DELETE FROM address_level_type where 1 = 1; DELETE FROM catchment where 1 = 1; @@ -74,3 +75,4 @@ ALTER SEQUENCE audit_id_seq RESTART WITH 1; ALTER SEQUENCE external_system_config_id_seq RESTART WITH 1; ALTER SEQUENCE identifier_source_id_seq RESTART WITH 1; ALTER SEQUENCE identifier_user_assignment_id_seq RESTART WITH 1; +ALTER SEQUENCE location_location_mapping_id_seq RESTART WITH 1; From 2a3ff72e6151f009f62b456a58bab8b108e8f7db Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 21 Aug 2024 18:08:01 +0530 Subject: [PATCH 111/129] #764 - dashboard filter voiding handled --- .../src/main/java/org/avni/server/service/DashboardService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java index abdf16134..f0bc2d6da 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/DashboardService.java @@ -69,6 +69,7 @@ private void uploadDashboardFilters(DashboardBundleContract bundleContract, Dash dashboardFilter = new DashboardFilter(); dashboardFilter.setUuid(bundleFilter.getUuid()); } + dashboardFilter.setVoided(bundleFilter.isVoided()); dashboardFilter.setName(bundleFilter.getName()); DashboardFilterConfigBundleContract bundleFilterConfig = bundleFilter.getFilterConfig(); dashboardFilter.setFilterConfig(bundleFilterConfig.toJsonObject()); From d934cf5e794ecd843c91bdc6cf783effcf141997 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 22 Aug 2024 11:26:46 +0530 Subject: [PATCH 112/129] avniproject/avni-webapp#1306 - location csv edit test, WIP --- .../batch/csv/writer/BulkLocationCreator.java | 6 ++- .../BuildLocationCreatorIntegrationTest.java | 53 +++++++++++++++++-- .../BulkLocationEditorIntegrationTest.java | 32 +++++++++++ 3 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java index 7fcb5f5fe..14ac862b4 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java @@ -31,6 +31,8 @@ public class BulkLocationCreator extends BulkLocationModifier { private final AddressLevelTypeRepository addressLevelTypeRepository; private final ImportService importService; private final FormService formService; + public static final String LocationTypesHeaderError = "Location types missing or not in order in header for specified Location Hierarchy. Please refer to sample file for valid list of headers."; + public static final String UnknownHeadersErrorMessage = "Unknown headers included in file. Please refer to sample file for valid list of headers."; public BulkLocationCreator(LocationService locationService, LocationRepository locationRepository, AddressLevelTypeRepository addressLevelTypeRepository, ObservationCreator observationCreator, ImportService importService, FormService formService) { super(locationRepository, observationCreator); @@ -74,7 +76,7 @@ private List checkIfHeaderHasLocationTypesInOrderForHierarchy(String loc .stream().map(AddressLevelType::getName).collect(Collectors.toList()); if (headerList.size() >= locationTypeNamesForHierachy.size() && !headerList.subList(0, locationTypeNamesForHierachy.size()).equals(locationTypeNamesForHierachy)) { - allErrorMsgs.add("Location types missing or not in order in header for specified Location Hierarchy. Please refer to sample file for valid list of headers."); + allErrorMsgs.add(LocationTypesHeaderError); throw new RuntimeException(String.join(", ", allErrorMsgs)); } return locationTypeNamesForHierachy; @@ -87,7 +89,7 @@ private void checkIfHeaderRowHasUnknownHeaders(List additionalHeaders, L .stream().map(FormElement::getName).collect(Collectors.toList()); locationPropertyNames.add(LocationHeaders.gpsCoordinates); if ((!locationPropertyNames.containsAll(additionalHeaders))) { - allErrorMsgs.add("Unknown headers included in file. Please refer to sample file for valid list of headers."); + allErrorMsgs.add(UnknownHeadersErrorMessage); throw new RuntimeException(String.join(", ", allErrorMsgs)); } } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java index 867c26340..7bec9ec8f 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java @@ -1,12 +1,13 @@ package org.avni.server.importer.batch.csv.writer; import org.avni.server.common.AbstractControllerIntegrationTest; -import org.avni.server.dao.AddressLevelTypeRepository; +import org.avni.server.dao.LocationRepository; import org.avni.server.domain.AddressLevelType; import org.avni.server.domain.factory.AddressLevelTypeBuilder; import org.avni.server.importer.batch.model.Row; import org.avni.server.service.LocationHierarchyService; import org.avni.server.service.builder.TestDataSetupService; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; @@ -16,6 +17,10 @@ import java.util.Map; import java.util.UUID; +import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.LocationTypesHeaderError; +import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.UnknownHeadersErrorMessage; +import static org.junit.Assert.assertEquals; + @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public class BuildLocationCreatorIntegrationTest extends AbstractControllerIntegrationTest { @@ -25,8 +30,12 @@ public class BuildLocationCreatorIntegrationTest extends AbstractControllerInteg private BulkLocationCreator bulkLocationCreator; @Autowired private LocationHierarchyService locationHierarchyService; + @Autowired + private LocationRepository addressLevelRepository; + private String hierarchy; @Test + @Ignore public void shouldCreate() { TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation(); AddressLevelType block = new AddressLevelTypeBuilder().name("Block").level(2d).withUuid(UUID.randomUUID()).build(); @@ -36,12 +45,48 @@ public void shouldCreate() { setUser(organisationData.getUser().getUsername()); Map hierarchies = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); - String hierarchy = String.join(".", hierarchies.keySet()); + hierarchy = String.join(".", hierarchies.keySet()); + String[] validHeaders = new String[]{"State", "District", "Block", "GPS coordinates"}; - success(validHeaders, hierarchy, "Bihar", "Vaishali", "Mahua", "23.45,43.85"); + success(validHeaders, 3, "Bihar", "Vaishali", "Mahua", "23.45,43.85"); + success(validHeaders, 1, " Bihar", " Vaishali ", " Jamui ", "23.20,43.85"); + success(validHeaders, 1, " Bihar", " Darbhanga "); + success(validHeaders, 1, " Bihar", " Aara ", " ", "24.20,43.85"); + success(new String[]{" State ", "District", " Block", " GPS coordinates"}, 1, " Bihar", " Chapra ", " ", "24.20,43.85"); + + success(validHeaders, 1, " bihar", " VAISHALI ", " Tarora ", "23.20,43.85"); + + failure(new String[]{"State1", "District", "Block", "GPS coordinates"}, LocationTypesHeaderError, "Bihar", "Vaishali", "Nijma", "23.45,43.85"); + failure(new String[]{"State", "District2", "Block", "GPS coordinates"}, LocationTypesHeaderError, "Bihar", "Vaishali", "Nijma", "23.45,43.85"); + failure(new String[]{"State", "District", "Block", "GPS"}, UnknownHeadersErrorMessage, "Bihar", "Vaishali", "Nijma", "23.45,43.85"); + failure(validHeaders, LocationTypesHeaderError, " ", " ", "Sori", "23.45,43.85"); + + successNoOp(validHeaders, "Ex: state 1", "Ex: distr 1", "Ex: blo 1", "Ex. 23.45,43.85"); + successNoOp(validHeaders, " Ex: state 1", "Ex: distr 1 ", "Ex: blo 1", " Ex. 23.45,43.85 "); + } + + private void successNoOp(String[] headers, String ... additionalHeaders) { + long before = addressLevelRepository.count(); + bulkLocationCreator.write(Collections.singletonList(new Row(headers, additionalHeaders)), hierarchy); + long after = addressLevelRepository.count(); + assertEquals(before, after); } - private void success(String[] headers, String hierarchy, String... cells) { + private void success(String[] headers, int numberOfNewLocations, String... cells) { + long before = addressLevelRepository.count(); bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); + long after = addressLevelRepository.count(); + assertEquals(before + numberOfNewLocations, after); + } + + private void failure(String[] headers, String errorMessage, String... cells) { + long before = addressLevelRepository.count(); + try { + bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); + } catch (Exception e) { + assertEquals(errorMessage, e.getMessage()); + } + long after = addressLevelRepository.count(); + assertEquals(before, after); } } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java new file mode 100644 index 000000000..68f97d79c --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java @@ -0,0 +1,32 @@ +package org.avni.server.importer.batch.csv.writer; + +import org.avni.server.common.AbstractControllerIntegrationTest; +import org.avni.server.domain.AddressLevelType; +import org.avni.server.domain.factory.AddressLevelTypeBuilder; +import org.avni.server.service.builder.TestDataSetupService; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +import java.util.Arrays; +import java.util.UUID; + +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +public class BulkLocationEditorIntegrationTest extends AbstractControllerIntegrationTest { + @Autowired + private TestDataSetupService testDataSetupService; + + @Test + @Ignore + public void shouldEdit() { + TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation(); + AddressLevelType block = new AddressLevelTypeBuilder().name("Block").level(2d).withUuid(UUID.randomUUID()).build(); + AddressLevelType district = new AddressLevelTypeBuilder().name("District").level(3d).withUuid(UUID.randomUUID()).build(); + AddressLevelType state = new AddressLevelTypeBuilder().name("State").level(4d).withUuid(UUID.randomUUID()).build(); + testDataSetupService.saveLocationTypes(Arrays.asList(block, district, state)); + + setUser(organisationData.getUser().getUsername()); + } +} From 5d07ce86828d8fbcaad93a2d9e4dfbe2e9484ebe Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 22 Aug 2024 12:26:56 +0530 Subject: [PATCH 113/129] avniproject/avni-webapp#1306 - more scenarios in BulkLocationCreatorIntegrationTest --- .../batch/csv/creator/ObservationCreator.java | 8 +- .../batch/csv/writer/BulkLocationCreator.java | 8 +- .../BuildLocationCreatorIntegrationTest.java | 92 ---------- .../BulkLocationCreatorIntegrationTest.java | 173 ++++++++++++++++++ 4 files changed, 181 insertions(+), 100 deletions(-) delete mode 100644 avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java create mode 100644 avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java index 4fb9eb409..137808e4d 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/ObservationCreator.java @@ -122,7 +122,7 @@ private boolean isNonEmptyQuestionGroup(FormElement formElement, Row row) { private String getRowValue(FormElement formElement, Row row, Integer questionGroupIndex) { Concept concept = formElement.getConcept(); - if(formElement.getGroup() != null) { + if (formElement.getGroup() != null) { Concept parentConcept = formElement.getGroup().getConcept(); String parentChildName = parentConcept.getName() + "|" + concept.getName(); String headerName = questionGroupIndex == null ? parentChildName : String.format("%s|%d", parentChildName, questionGroupIndex); @@ -136,7 +136,7 @@ private ObservationCollection constructObservations(Row row, Headers headers, Li for (Concept concept : getConceptHeaders(headers, row.getHeaders())) { FormElement formElement = getFormElementForObservationConcept(concept, formType); String rowValue = getRowValue(formElement, row, null); - if (!isNonEmptyQuestionGroup(formElement, row) && (rowValue == null || rowValue.trim().equals(""))) + if (!isNonEmptyQuestionGroup(formElement, row) && (rowValue == null || rowValue.trim().isEmpty())) continue; ObservationRequest observationRequest = new ObservationRequest(); observationRequest.setConceptName(concept.getName()); @@ -166,7 +166,7 @@ private Object constructChildObservations(Row row, Headers headers, List List repeatableObservationRequest = new ArrayList<>(); for (int i = 1; i <= maxIndex; i++) { ObservationCollection questionGroupObservations = getQuestionGroupObservations(row, headers, errorMsgs, formType, oldObservations, allChildQuestions, i); - if(!questionGroupObservations.isEmpty()) { + if (!questionGroupObservations.isEmpty()) { repeatableObservationRequest.add(questionGroupObservations); } } @@ -205,7 +205,7 @@ private List createDecisionFormElement(Set concepts) { }).collect(Collectors.toList()); } - private FormElement getFormElementForObservationConcept(Concept concept, FormType formType) { + private FormElement getFormElementForObservationConcept(Concept concept, FormType formType) { List applicableForms = formRepository.findByFormTypeAndIsVoidedFalse(formType); if (applicableForms.isEmpty()) throw new RuntimeException(String.format("No forms of type %s found", formType)); diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java index 14ac862b4..a112081fb 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java @@ -52,12 +52,12 @@ public List getLocationTypeNames() { public void createLocation(Row row, List allErrorMsgs, List locationTypeNames) { AddressLevel parent = null; AddressLevel location = null; - for (String header : row.getHeaders()) { - if (isValidLocation(header, row, locationTypeNames)) { - location = createAddressLevel(row, parent, header, locationTypeNames); + for (String columnHeader : row.getHeaders()) { + if (isValidLocation(columnHeader, row, locationTypeNames)) { + location = createAddressLevel(row, parent, columnHeader, locationTypeNames); parent = location; } //This will get called only when location have extra properties - if (location != null && !locationTypeNames.contains(header)) { + if (location != null && !locationTypeNames.contains(columnHeader)) { updateLocationProperties(row, allErrorMsgs, location); } } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java deleted file mode 100644 index 7bec9ec8f..000000000 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BuildLocationCreatorIntegrationTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.avni.server.importer.batch.csv.writer; - -import org.avni.server.common.AbstractControllerIntegrationTest; -import org.avni.server.dao.LocationRepository; -import org.avni.server.domain.AddressLevelType; -import org.avni.server.domain.factory.AddressLevelTypeBuilder; -import org.avni.server.importer.batch.model.Row; -import org.avni.server.service.LocationHierarchyService; -import org.avni.server.service.builder.TestDataSetupService; -import org.junit.Ignore; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.jdbc.Sql; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; -import java.util.UUID; - -import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.LocationTypesHeaderError; -import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.UnknownHeadersErrorMessage; -import static org.junit.Assert.assertEquals; - -@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) -@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) -public class BuildLocationCreatorIntegrationTest extends AbstractControllerIntegrationTest { - @Autowired - private TestDataSetupService testDataSetupService; - @Autowired - private BulkLocationCreator bulkLocationCreator; - @Autowired - private LocationHierarchyService locationHierarchyService; - @Autowired - private LocationRepository addressLevelRepository; - private String hierarchy; - - @Test - @Ignore - public void shouldCreate() { - TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation(); - AddressLevelType block = new AddressLevelTypeBuilder().name("Block").level(2d).withUuid(UUID.randomUUID()).build(); - AddressLevelType district = new AddressLevelTypeBuilder().name("District").level(3d).withUuid(UUID.randomUUID()).build(); - AddressLevelType state = new AddressLevelTypeBuilder().name("State").level(4d).withUuid(UUID.randomUUID()).build(); - testDataSetupService.saveLocationTypes(Arrays.asList(block, district, state)); - - setUser(organisationData.getUser().getUsername()); - Map hierarchies = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); - hierarchy = String.join(".", hierarchies.keySet()); - - String[] validHeaders = new String[]{"State", "District", "Block", "GPS coordinates"}; - success(validHeaders, 3, "Bihar", "Vaishali", "Mahua", "23.45,43.85"); - success(validHeaders, 1, " Bihar", " Vaishali ", " Jamui ", "23.20,43.85"); - success(validHeaders, 1, " Bihar", " Darbhanga "); - success(validHeaders, 1, " Bihar", " Aara ", " ", "24.20,43.85"); - success(new String[]{" State ", "District", " Block", " GPS coordinates"}, 1, " Bihar", " Chapra ", " ", "24.20,43.85"); - - success(validHeaders, 1, " bihar", " VAISHALI ", " Tarora ", "23.20,43.85"); - - failure(new String[]{"State1", "District", "Block", "GPS coordinates"}, LocationTypesHeaderError, "Bihar", "Vaishali", "Nijma", "23.45,43.85"); - failure(new String[]{"State", "District2", "Block", "GPS coordinates"}, LocationTypesHeaderError, "Bihar", "Vaishali", "Nijma", "23.45,43.85"); - failure(new String[]{"State", "District", "Block", "GPS"}, UnknownHeadersErrorMessage, "Bihar", "Vaishali", "Nijma", "23.45,43.85"); - failure(validHeaders, LocationTypesHeaderError, " ", " ", "Sori", "23.45,43.85"); - - successNoOp(validHeaders, "Ex: state 1", "Ex: distr 1", "Ex: blo 1", "Ex. 23.45,43.85"); - successNoOp(validHeaders, " Ex: state 1", "Ex: distr 1 ", "Ex: blo 1", " Ex. 23.45,43.85 "); - } - - private void successNoOp(String[] headers, String ... additionalHeaders) { - long before = addressLevelRepository.count(); - bulkLocationCreator.write(Collections.singletonList(new Row(headers, additionalHeaders)), hierarchy); - long after = addressLevelRepository.count(); - assertEquals(before, after); - } - - private void success(String[] headers, int numberOfNewLocations, String... cells) { - long before = addressLevelRepository.count(); - bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); - long after = addressLevelRepository.count(); - assertEquals(before + numberOfNewLocations, after); - } - - private void failure(String[] headers, String errorMessage, String... cells) { - long before = addressLevelRepository.count(); - try { - bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); - } catch (Exception e) { - assertEquals(errorMessage, e.getMessage()); - } - long after = addressLevelRepository.count(); - assertEquals(before, after); - } -} diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java new file mode 100644 index 000000000..935fd41f7 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java @@ -0,0 +1,173 @@ +package org.avni.server.importer.batch.csv.writer; + +import org.avni.server.common.AbstractControllerIntegrationTest; +import org.avni.server.dao.LocationRepository; +import org.avni.server.domain.AddressLevel; +import org.avni.server.domain.AddressLevelType; +import org.avni.server.domain.ConceptDataType; +import org.avni.server.domain.factory.AddressLevelTypeBuilder; +import org.avni.server.importer.batch.model.Row; +import org.avni.server.service.LocationHierarchyService; +import org.avni.server.service.builder.TestConceptService; +import org.avni.server.service.builder.TestDataSetupService; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +import java.util.*; + +import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.LocationTypesHeaderError; +import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.UnknownHeadersErrorMessage; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +public class BulkLocationCreatorIntegrationTest extends AbstractControllerIntegrationTest { + @Autowired + private TestDataSetupService testDataSetupService; + @Autowired + private BulkLocationCreator bulkLocationCreator; + @Autowired + private LocationHierarchyService locationHierarchyService; + @Autowired + private LocationRepository addressLevelRepository; + @Autowired + private TestConceptService testConceptService; + @Autowired + private LocationRepository locationRepository; + private String hierarchy; + + @Override + public void setUp() throws Exception { + TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation(); + AddressLevelType block = new AddressLevelTypeBuilder().name("Block").level(2d).withUuid(UUID.randomUUID()).build(); + AddressLevelType district = new AddressLevelTypeBuilder().name("District").level(3d).withUuid(UUID.randomUUID()).build(); + AddressLevelType state = new AddressLevelTypeBuilder().name("State").level(4d).withUuid(UUID.randomUUID()).build(); + testDataSetupService.saveLocationTypes(Arrays.asList(block, district, state)); + testConceptService.createCodedConcept("Coded Concept", "Answer 1", "Answer 2"); + testConceptService.createConcept("Text Concept", ConceptDataType.Text); + + setUser(organisationData.getUser().getUsername()); + Map hierarchies = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); + hierarchy = String.join(".", hierarchies.keySet()); + } + + private static int newLocationsCreated(int count) { + return count; + } + + private static String[] header(String ... cells) { + return cells; + } + + private static String[] dataRow(String ... cells) { + return cells; + } + + private void treatAsDescriptor(String[] headers, String ... additionalHeaders) { + long before = addressLevelRepository.count(); + bulkLocationCreator.write(Collections.singletonList(new Row(headers, additionalHeaders)), hierarchy); + long after = addressLevelRepository.count(); + assertEquals(before, after); + } + + private void success(String[] headers, String[] cells, int numberOfNewLocations) { + long before = addressLevelRepository.count(); + bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); + long after = addressLevelRepository.count(); + assertEquals(before + newLocationsCreated(numberOfNewLocations), after); + } + + private void failure(String[] headers, String[] cells, String errorMessage) { + long before = addressLevelRepository.count(); + try { + bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); + } catch (Exception e) { + assertEquals(errorMessage, e.getMessage()); + } + long after = addressLevelRepository.count(); + assertEquals(before, after); + } + + private String[] lineage(String ... lineage) { + return lineage; + } + + private void lineageExists(String ... lineage) { + AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(".", lineage)).get(); + assertNotNull(address); + } + + private void locationHasAttribute(String[] lineage, String concept) { + AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(".", lineage)).get(); + assertNotNull(address.getLocationProperties().get(concept)); + } + + @Test + @Ignore + public void shouldCreate() { + success(header("State", "District", "Block", "GPS coordinates"), + dataRow("Bihar", "Vaishali", "Mahua", "23.45,43.85"), + newLocationsCreated(3)); + success(header("State", "District", "Block", "GPS coordinates"), + dataRow(" Bihar", " Vaishali ", " Jamui ", "23.20,43.85"), + newLocationsCreated(1)); + success(header("State", "District", "Block", "GPS coordinates"), + dataRow(" Bihar", " Darbhanga "), + newLocationsCreated(1)); + success(header("State", "District", "Block", "GPS coordinates"), + dataRow(" Bihar", " Aara ", " ", "24.20,43.85"), + newLocationsCreated(1)); + success(header(" State ", "District", " Block", " GPS coordinates"), + dataRow(" Bihar", " Chapra ", " ", "24.20,43.85"), + newLocationsCreated(1)); + success(header("State", "District", "Block", "GPS coordinates"), + dataRow(" bihar", " VAISHALI ", " Tarora ", "23.20,43.85"), + newLocationsCreated(1)); + + failure(header("State1", "District", "Block", "GPS coordinates"), + dataRow("Bihar", "Vaishali", "Nijma", "23.45,43.85"), + LocationTypesHeaderError); + failure(header("State", "District2", "Block", "GPS coordinates"), + dataRow("Bihar", "Vaishali", "Nijma", "23.45,43.85"), + LocationTypesHeaderError); + failure(header("State", "District", "Block", "GPS"), + dataRow("Bihar", "Vaishali", "Nijma", "23.45,43.85"), + UnknownHeadersErrorMessage); + + treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), + dataRow("Ex: state 1", "Ex: distr 1", "Ex: blo 1", "Ex. 23.45,43.85")); + treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), + dataRow(" Ex: state 1", "Ex: distr 1 ", "Ex: blo 1", " Ex. 23.45,43.85 ")); + + + //attributes + success(header("State", "District", "Block", "GPS coordinates", "Coded Concept"), + dataRow("Bihar", "Vaishali", "Block 1", "23.45,43.86", "Answer 1"), + newLocationsCreated(1)); + locationHasAttribute(lineage("Bihar", "Vaishali", "Block 1"), "Coded Concept"); + + success(header("State", "District", "Block", "GPS coordinates", " Coded Concept"), + dataRow("Bihar", "Vaishali", "Block 2", "23.45,43.86", " Answer 1"), + newLocationsCreated(1)); + locationHasAttribute(lineage("Bihar", "Vaishali", "Block 2"), "Coded Concept"); + + success(header("State", "District", "Block", "GPS coordinates", "Text Concept"), + dataRow("Bihar", "Vaishali", "Block 3", "23.45,43.86", "any text"), + newLocationsCreated(1)); + locationHasAttribute(lineage("Bihar", "Vaishali", "Block 3"), "Text Concept"); + + failure(header("State", "District", "Block", "GPS coordinates", "Coded Concept "), + dataRow("Bihar", "Vaishali", "Block 4", "23.45,43.86", "not an answer to this concept"), + ""); + + // in random steps + success(header("State", "District", "Block", "GPS coordinates"), + dataRow(" ", " ", "Block11", "23.45,43.85"), newLocationsCreated(1)); + success(header("State", "District", "Block", "GPS coordinates"), + dataRow("Bihar", "District1", "Block11"), newLocationsCreated(1)); + lineageExists("Bihar", "District1", "Block11"); + } +} From 711c2506383c82c4f59e0624bd07a8b1d4e665b2 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 22 Aug 2024 13:20:31 +0530 Subject: [PATCH 114/129] avniproject/avni-webapp#1306 - fixed scenarios in BulkLocationCreatorIntegrationTest --- .../batch/csv/writer/BaseCSVImportTest.java | 20 ++++++ .../BulkLocationCreatorIntegrationTest.java | 63 +++++++++++-------- .../BulkLocationEditorIntegrationTest.java | 9 ++- 3 files changed, 65 insertions(+), 27 deletions(-) create mode 100644 avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java new file mode 100644 index 000000000..125ea6ba5 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java @@ -0,0 +1,20 @@ +package org.avni.server.importer.batch.csv.writer; + +import org.avni.server.common.AbstractControllerIntegrationTest; +import org.avni.server.domain.AddressLevel; + +import static org.junit.Assert.assertNotNull; + +public abstract class BaseCSVImportTest extends AbstractControllerIntegrationTest { + protected String[] header(String... cells) { + return cells; + } + + protected String[] dataRow(String... cells) { + return cells; + } + + protected String[] lineage(String ... lineage) { + return lineage; + } +} diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java index 935fd41f7..4d1112d60 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java @@ -1,6 +1,5 @@ package org.avni.server.importer.batch.csv.writer; -import org.avni.server.common.AbstractControllerIntegrationTest; import org.avni.server.dao.LocationRepository; import org.avni.server.domain.AddressLevel; import org.avni.server.domain.AddressLevelType; @@ -15,7 +14,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.LocationTypesHeaderError; import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.UnknownHeadersErrorMessage; @@ -24,7 +26,7 @@ @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) -public class BulkLocationCreatorIntegrationTest extends AbstractControllerIntegrationTest { +public class BulkLocationCreatorIntegrationTest extends BaseCSVImportTest { @Autowired private TestDataSetupService testDataSetupService; @Autowired @@ -58,12 +60,14 @@ private static int newLocationsCreated(int count) { return count; } - private static String[] header(String ... cells) { - return cells; + private void lineageExists(String ... lineage) { + AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(".", lineage)).get(); + assertNotNull(address); } - private static String[] dataRow(String ... cells) { - return cells; + private void locationHasAttribute(String[] lineage, String concept) { + AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(".", lineage)).get(); + assertNotNull(address.getLocationProperties().get(concept)); } private void treatAsDescriptor(String[] headers, String ... additionalHeaders) { @@ -91,20 +95,6 @@ private void failure(String[] headers, String[] cells, String errorMessage) { assertEquals(before, after); } - private String[] lineage(String ... lineage) { - return lineage; - } - - private void lineageExists(String ... lineage) { - AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(".", lineage)).get(); - assertNotNull(address); - } - - private void locationHasAttribute(String[] lineage, String concept) { - AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(".", lineage)).get(); - assertNotNull(address.getLocationProperties().get(concept)); - } - @Test @Ignore public void shouldCreate() { @@ -163,11 +153,34 @@ public void shouldCreate() { dataRow("Bihar", "Vaishali", "Block 4", "23.45,43.86", "not an answer to this concept"), ""); - // in random steps + success(header("State", "District", "Block", "GPS coordinates", " Coded Concept", "Text Concept"), + dataRow("Bihar", "Vaishali", "Block 5", "23.45,43.86", " Answer 1", "any text"), + newLocationsCreated(1)); + locationHasAttribute(lineage("Bihar", "Vaishali", "Block 5"), "Coded Concept"); + locationHasAttribute(lineage("Bihar", "Vaishali", "Block 5"), "Text Concept"); + // end + + + // without full hierarchy + success(header("State", "District", "Block", "GPS coordinates"), + dataRow("Bihar", "District 1", " ", "23.45,43.85"), newLocationsCreated(1)); + lineageExists("Bihar", "District 1"); + success(header("State", "District", "Block", "GPS coordinates"), - dataRow(" ", " ", "Block11", "23.45,43.85"), newLocationsCreated(1)); + dataRow("State 2", "District 1", " ", "23.45,43.85"), newLocationsCreated(2)); + lineageExists("State 2", "District 1"); + success(header("State", "District", "Block", "GPS coordinates"), - dataRow("Bihar", "District1", "Block11"), newLocationsCreated(1)); - lineageExists("Bihar", "District1", "Block11"); + dataRow("State 3", " ", " ", "23.45,43.85"), newLocationsCreated(1)); + lineageExists("State 3"); + // end + + + // if in random steps + failure(header("State", "District", "Block", "GPS coordinates"), + dataRow(" ", " ", "Block11", "23.45,43.85"), ""); + failure(header("State", "District", "Block", "GPS coordinates"), + dataRow(" ", " District 11", "Block11", "23.45,43.85"), ""); + // end } } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java index 68f97d79c..8dde05c5b 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java @@ -1,8 +1,9 @@ package org.avni.server.importer.batch.csv.writer; -import org.avni.server.common.AbstractControllerIntegrationTest; import org.avni.server.domain.AddressLevelType; +import org.avni.server.domain.ConceptDataType; import org.avni.server.domain.factory.AddressLevelTypeBuilder; +import org.avni.server.service.builder.TestConceptService; import org.avni.server.service.builder.TestDataSetupService; import org.junit.Ignore; import org.junit.Test; @@ -14,9 +15,11 @@ @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) -public class BulkLocationEditorIntegrationTest extends AbstractControllerIntegrationTest { +public class BulkLocationEditorIntegrationTest extends BaseCSVImportTest { @Autowired private TestDataSetupService testDataSetupService; + @Autowired + private TestConceptService testConceptService; @Test @Ignore @@ -26,6 +29,8 @@ public void shouldEdit() { AddressLevelType district = new AddressLevelTypeBuilder().name("District").level(3d).withUuid(UUID.randomUUID()).build(); AddressLevelType state = new AddressLevelTypeBuilder().name("State").level(4d).withUuid(UUID.randomUUID()).build(); testDataSetupService.saveLocationTypes(Arrays.asList(block, district, state)); + testConceptService.createCodedConcept("Coded Concept", "Answer 1", "Answer 2"); + testConceptService.createConcept("Text Concept", ConceptDataType.Text); setUser(organisationData.getUser().getUsername()); } From bbf21615f86b6a7ca4e191348c8c3aa1e8b5b6ac Mon Sep 17 00:00:00 2001 From: Joy A Date: Thu, 22 Aug 2024 14:56:08 +0530 Subject: [PATCH 115/129] avniproject/avni-webapp#1290 | Error message for address level type deletion --- .../java/org/avni/server/web/AddressLevelTypeController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/AddressLevelTypeController.java b/avni-server-api/src/main/java/org/avni/server/web/AddressLevelTypeController.java index d095f9de7..5a41c7a73 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/AddressLevelTypeController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/AddressLevelTypeController.java @@ -118,7 +118,7 @@ public ResponseEntity voidAddressLevelType(@PathVariable("id") Long id) { } if (!addressLevelType.isVoidable()) { return ResponseEntity.badRequest().body(ReactAdminUtil.generateJsonError( - String.format("Cannot delete Type '%s' until all SubTypes are deleted or there are non-voided addresses depending on it", addressLevelType.getName()))); + String.format("Cannot delete '%s'. Sub location types or locations of this type exist. Please delete them to proceed.", addressLevelType.getName()))); } addressLevelType.setVoided(true); return new ResponseEntity<>(addressLevelType, HttpStatus.OK); From e05078d1ea6638c0a78eee191f115c9c5c56aa4b Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 23 Aug 2024 11:36:57 +0530 Subject: [PATCH 116/129] avniproject/avni-webapp#1306 - validate row for descriptor header, trailing empty locations only, no locations provided, fix for space in header. Completed test for location create. --- .../avni/server/dao/LocationRepository.java | 18 ++-- .../batch/csv/writer/BulkLocationCreator.java | 33 ++++++- .../avni/server/importer/batch/model/Row.java | 8 +- .../service/ImportLocationsConstants.java | 2 +- .../org/avni/server/util/CollectionUtil.java | 26 ++++++ .../server/application/TestFormBuilder.java | 16 ---- .../application/TestFormElementBuilder.java | 30 +++++++ .../TestFormElementGroupBuilder.java | 40 ++++++++- .../factory/metadata/TestFormBuilder.java | 10 +++ .../batch/csv/writer/BaseCSVImportTest.java | 4 + .../BulkLocationCreatorIntegrationTest.java | 88 +++++++++++++------ ...edValidationServiceQuestionGroupsTest.java | 1 + .../service/ObservationServiceTest.java | 1 + .../avni/server/util/CollectionUtilTest.java | 17 ++++ 14 files changed, 239 insertions(+), 55 deletions(-) delete mode 100644 avni-server-api/src/test/java/org/avni/server/application/TestFormBuilder.java create mode 100644 avni-server-api/src/test/java/org/avni/server/util/CollectionUtilTest.java diff --git a/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java index a95e80b26..0669aa305 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java @@ -57,12 +57,12 @@ public interface LocationRepository extends ReferenceDataRepository findLocationProjectionByTitleIgnoreCaseAndTypeId(String title, Integer typeId, Pageable pageable); @Query(value = findLocationProjectionByTitleIgnoreCaseAndTypeIdQuery, - nativeQuery = true) + nativeQuery = true) List findLocationProjectionByTitleIgnoreCaseAndTypeIdAsList(String title, Integer typeId); String addressLevelParentClause = " and al.parent_id = :parentId "; @@ -153,6 +153,7 @@ default AddressLevel findChildLocation(String title, String type, String parentN } AddressLevel findByTitleIgnoreCaseAndTypeNameAndIsVoidedFalse(String title, String type); + default AddressLevel findLocation(String title, String type) { return this.findByTitleIgnoreCaseAndTypeNameAndIsVoidedFalse(title, type); } @@ -171,7 +172,7 @@ List findByType_IdAndTitleIgnoreCaseStartingWithAndIsVoidedFalseOr Long getAddressIdByLineage(String locationTitleLineage); default Optional findByTitleLineageIgnoreCase(String locationTitleLineage) { - if(!StringUtils.hasText(locationTitleLineage)) { + if (!StringUtils.hasText(locationTitleLineage)) { return Optional.empty(); } Long addressId = getAddressIdByLineage(locationTitleLineage); @@ -259,6 +260,7 @@ default Optional findByTitleLineageIgnoreCase(String locationTitle List getParents(String uuid); List findByTitleAndType(String title, AddressLevelType lowestAddressLevelType, Pageable pageable); + AddressLevel findByTitleAndTypeAndIsVoidedFalse(String title, AddressLevelType addressLevelType); List findAllByIdIn(List addressIds); diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java index a112081fb..3c3b75277 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java @@ -10,13 +10,16 @@ import org.avni.server.importer.batch.csv.writer.header.LocationHeaders; import org.avni.server.importer.batch.model.Row; import org.avni.server.service.FormService; +import org.avni.server.service.ImportLocationsConstants; import org.avni.server.service.ImportService; import org.avni.server.service.LocationService; +import org.avni.server.util.CollectionUtil; import org.avni.server.util.S; import org.avni.server.web.request.LocationContract; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import javax.transaction.Transactional; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -33,6 +36,8 @@ public class BulkLocationCreator extends BulkLocationModifier { private final FormService formService; public static final String LocationTypesHeaderError = "Location types missing or not in order in header for specified Location Hierarchy. Please refer to sample file for valid list of headers."; public static final String UnknownHeadersErrorMessage = "Unknown headers included in file. Please refer to sample file for valid list of headers."; + public static final String ParentMissingOfLocation = "Parent missing for location provided"; + public static final String NoLocationProvided = "No location provided"; public BulkLocationCreator(LocationService locationService, LocationRepository locationRepository, AddressLevelTypeRepository addressLevelTypeRepository, ObservationCreator observationCreator, ImportService importService, FormService formService) { super(locationRepository, observationCreator); @@ -116,11 +121,33 @@ private boolean isValidLocation(String header, Row row, List locationTyp return locationTypeNames.contains(header) && !S.isEmpty(row.get(header)); } - public void write(List rows, String locationHierarchy) { + private void validateRow(Row row, List hierarchicalLocationTypeNames, List allErrorMsgs) { + List values = row.get(hierarchicalLocationTypeNames); + if (CollectionUtil.isEmpty(values)) { + allErrorMsgs.add(NoLocationProvided); + throw new RuntimeException(String.join(", ", allErrorMsgs)); + } + if (!CollectionUtil.hasOnlyTrailingEmptyStrings(values)) { + allErrorMsgs.add(ParentMissingOfLocation); + throw new RuntimeException(String.join(", ", allErrorMsgs)); + } + } + + @Transactional(Transactional.TxType.REQUIRES_NEW) + public void write(List rows, String idBasedLocationHierarchy) { List allErrorMsgs = new ArrayList<>(); - List locationTypeNames = validateCreateModeHeaders(rows.get(0).getHeaders(), allErrorMsgs, locationHierarchy); + List hierarchicalLocationTypeNames = validateCreateModeHeaders(rows.get(0).getHeaders(), allErrorMsgs, idBasedLocationHierarchy); for (Row row : rows) { - createLocation(row, allErrorMsgs, locationTypeNames); + if (skipRow(row, hierarchicalLocationTypeNames)) { + continue; + } + validateRow(row, hierarchicalLocationTypeNames, allErrorMsgs); + createLocation(row, allErrorMsgs, hierarchicalLocationTypeNames); } } + + private boolean skipRow(Row row, List hierarchicalLocationTypeNames) { + List values = row.get(hierarchicalLocationTypeNames); + return CollectionUtil.anyStartsWith(values, ImportLocationsConstants.Example); + } } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/model/Row.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/model/Row.java index d5123dbe8..c76d5bc41 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/model/Row.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/model/Row.java @@ -2,7 +2,9 @@ import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.IntStream; import static java.lang.String.format; @@ -15,7 +17,7 @@ public class Row extends HashMap { public Row(String[] headers, String[] values) { this.headers = headers; this.values = values; - IntStream.range(0, values.length).forEach(index -> this.put(headers[index], values[index].trim())); + IntStream.range(0, values.length).forEach(index -> this.put(headers[index].trim(), values[index].trim())); } private String nullSafeTrim(String s) { @@ -29,6 +31,10 @@ public String[] getHeaders() { return Arrays.stream(headers).map(this::nullSafeTrim).toArray(String[]::new); } + public List get(List headers) { + return headers.stream().map(this::get).collect(Collectors.toList()); + } + @Override public String get(Object key) { String k = nullSafeTrim((String) key); diff --git a/avni-server-api/src/main/java/org/avni/server/service/ImportLocationsConstants.java b/avni-server-api/src/main/java/org/avni/server/service/ImportLocationsConstants.java index 75855e454..91d951386 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ImportLocationsConstants.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ImportLocationsConstants.java @@ -10,7 +10,7 @@ public interface ImportLocationsConstants { String STRING_CONSTANT_EMPTY_STRING = ""; String STRING_PLACEHOLDER_BLOCK = "\"%s\","; String STRING_3_PLACEHOLDER_BLOCK = String.join("", Collections.nCopies(3, STRING_PLACEHOLDER_BLOCK)); - String Example = "Ex: "; + String Example = "Example: "; String ALLOWED_VALUES = "Allowed values: "; String COLUMN_NAME_GPS_COORDINATES = LocationHeaders.gpsCoordinates; String COLUMN_NAME_LOCATION_WITH_FULL_HIERARCHY = "Location with full hierarchy"; diff --git a/avni-server-api/src/main/java/org/avni/server/util/CollectionUtil.java b/avni-server-api/src/main/java/org/avni/server/util/CollectionUtil.java index fb04bcf9a..e2a0cf818 100644 --- a/avni-server-api/src/main/java/org/avni/server/util/CollectionUtil.java +++ b/avni-server-api/src/main/java/org/avni/server/util/CollectionUtil.java @@ -1,6 +1,7 @@ package org.avni.server.util; import org.avni.server.domain.CHSBaseEntity; +import org.springframework.util.StringUtils; import java.util.List; @@ -8,4 +9,29 @@ public class CollectionUtil { public static T findByUuid(List list, String uuid) { return (T) list.stream().filter(x -> x.getUuid().equals(uuid)).findFirst().orElse(null); } + + private static List trimEnd(List values) { + int i = values.size() - 1; + while (i >= 0 && StringUtils.isEmpty(values.get(i))) { + i--; + } + return values.subList(0, i + 1); + } + + private static boolean hasNoEmptyStrings(List values) { + return values.stream().noneMatch(StringUtils::isEmpty); + } + + public static boolean isEmpty(List values) { + return values.stream().allMatch(StringUtils::isEmpty); + } + + public static boolean hasOnlyTrailingEmptyStrings(List values) { + List trimmed = trimEnd(values); + return hasNoEmptyStrings(trimmed); + } + + public static boolean anyStartsWith(List values, String prefix) { + return values.stream().anyMatch(x -> !StringUtils.isEmpty(x) && x.startsWith(prefix)); + } } diff --git a/avni-server-api/src/test/java/org/avni/server/application/TestFormBuilder.java b/avni-server-api/src/test/java/org/avni/server/application/TestFormBuilder.java deleted file mode 100644 index 3e3587917..000000000 --- a/avni-server-api/src/test/java/org/avni/server/application/TestFormBuilder.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.avni.server.application; - -import java.util.Arrays; - -public class TestFormBuilder { - private final Form form = new Form(); - - public TestFormBuilder addFormElementGroup(FormElementGroup ... formElementGroups ) { - Arrays.stream(formElementGroups).forEach(form::addFormElementGroup); - return this; - } - - public Form build() { - return form; - } -} diff --git a/avni-server-api/src/test/java/org/avni/server/application/TestFormElementBuilder.java b/avni-server-api/src/test/java/org/avni/server/application/TestFormElementBuilder.java index 5861b16f0..c525e16ac 100644 --- a/avni-server-api/src/test/java/org/avni/server/application/TestFormElementBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/application/TestFormElementBuilder.java @@ -14,6 +14,11 @@ public TestFormElementBuilder withUuid(String uuid) { return this; } + public TestFormElementBuilder withName(String name) { + formElement.setName(name); + return this; + } + public TestFormElementBuilder withId(long id) { formElement.setId(id); return this; @@ -39,6 +44,31 @@ public TestFormElementBuilder withType(FormElementType type) { return this; } + public TestFormElementBuilder withDisplayOrder(Double displayOrder) { + formElement.setDisplayOrder(displayOrder); + return this; + } + + public TestFormElementBuilder withIsVoided(boolean isVoided) { + formElement.setVoided(isVoided); + return this; + } + + public TestFormElementBuilder withRule(String rule) { + formElement.setRule(rule); + return this; + } + + public TestFormElementBuilder withMandatory(boolean isMandatory) { + formElement.setMandatory(isMandatory); + return this; + } + + public TestFormElementBuilder withType(String type) { + formElement.setType(type); + return this; + } + public FormElement build() { return formElement; } diff --git a/avni-server-api/src/test/java/org/avni/server/application/TestFormElementGroupBuilder.java b/avni-server-api/src/test/java/org/avni/server/application/TestFormElementGroupBuilder.java index f597862b9..b3e0407d8 100644 --- a/avni-server-api/src/test/java/org/avni/server/application/TestFormElementGroupBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/application/TestFormElementGroupBuilder.java @@ -6,10 +6,48 @@ public class TestFormElementGroupBuilder { private final FormElementGroup formElementGroup = new FormElementGroup(); public TestFormElementGroupBuilder addFormElement(FormElement ... formElements) { - Arrays.stream(formElements).forEach(formElementGroup::addFormElement); + Arrays.stream(formElements).forEach(formElement -> { + formElementGroup.addFormElement(formElement); + formElement.setFormElementGroup(formElementGroup); + }); return this; } + public TestFormElementGroupBuilder withUuid(String uuid) { + formElementGroup.setUuid(uuid); + return this; + } + + public TestFormElementGroupBuilder withName(String name) { + formElementGroup.setName(name); + return this; + } + + public TestFormElementGroupBuilder withDisplayOrder(Double displayOrder) { + formElementGroup.setDisplayOrder(displayOrder); + return this; + } + + public TestFormElementGroupBuilder withIsVoided(boolean isVoided) { + formElementGroup.setVoided(isVoided); + return this; + } + + public TestFormElementGroupBuilder withDisplay(String display) { + formElementGroup.setDisplay(display); + return this; + } + + public TestFormElementGroupBuilder withRule(String rule) { + formElementGroup.setRule(rule); + return this; + } + + public TestFormElementGroupBuilder withTimed(boolean isTimed) { + formElementGroup.setTimed(isTimed); + return this; + } + public FormElementGroup build() { return formElementGroup; } diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/metadata/TestFormBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/metadata/TestFormBuilder.java index 498ccec0c..0d0eaf859 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/metadata/TestFormBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/metadata/TestFormBuilder.java @@ -1,8 +1,10 @@ package org.avni.server.domain.factory.metadata; import org.avni.server.application.Form; +import org.avni.server.application.FormElementGroup; import org.avni.server.application.FormType; +import java.util.Arrays; import java.util.UUID; public class TestFormBuilder { @@ -28,6 +30,14 @@ public TestFormBuilder withDefaultFieldsForNewEntity() { return withUuid(s).withName(s).withFormType(FormType.IndividualProfile); } + public TestFormBuilder addFormElementGroup(FormElementGroup... formElementGroups ) { + Arrays.stream(formElementGroups).forEach(formElementGroup -> { + entity.addFormElementGroup(formElementGroup); + formElementGroup.setForm(entity); + }); + return this; + } + public Form build() { return entity; } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java index 125ea6ba5..e9ef5976b 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java @@ -17,4 +17,8 @@ protected String[] dataRow(String... cells) { protected String[] lineage(String ... lineage) { return lineage; } + + protected String error(String message) { + return message; + } } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java index 4d1112d60..cca4d1939 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java @@ -1,28 +1,31 @@ package org.avni.server.importer.batch.csv.writer; +import org.avni.server.application.*; +import org.avni.server.dao.ConceptRepository; import org.avni.server.dao.LocationRepository; +import org.avni.server.dao.application.FormRepository; import org.avni.server.domain.AddressLevel; import org.avni.server.domain.AddressLevelType; +import org.avni.server.domain.Concept; import org.avni.server.domain.ConceptDataType; import org.avni.server.domain.factory.AddressLevelTypeBuilder; +import org.avni.server.domain.factory.metadata.TestFormBuilder; import org.avni.server.importer.batch.model.Row; import org.avni.server.service.LocationHierarchyService; import org.avni.server.service.builder.TestConceptService; import org.avni.server.service.builder.TestDataSetupService; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; +import wiremock.org.checkerframework.checker.units.qual.A; import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.UUID; -import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.LocationTypesHeaderError; -import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.UnknownHeadersErrorMessage; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.avni.server.importer.batch.csv.writer.BulkLocationCreator.*; +import static org.junit.Assert.*; @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @@ -39,6 +42,10 @@ public class BulkLocationCreatorIntegrationTest extends BaseCSVImportTest { private TestConceptService testConceptService; @Autowired private LocationRepository locationRepository; + @Autowired + private FormRepository formRepository; + @Autowired + private ConceptRepository conceptRepository; private String hierarchy; @Override @@ -48,8 +55,24 @@ public void setUp() throws Exception { AddressLevelType district = new AddressLevelTypeBuilder().name("District").level(3d).withUuid(UUID.randomUUID()).build(); AddressLevelType state = new AddressLevelTypeBuilder().name("State").level(4d).withUuid(UUID.randomUUID()).build(); testDataSetupService.saveLocationTypes(Arrays.asList(block, district, state)); - testConceptService.createCodedConcept("Coded Concept", "Answer 1", "Answer 2"); - testConceptService.createConcept("Text Concept", ConceptDataType.Text); + Concept codedConcept = testConceptService.createCodedConcept("Coded Concept", "Answer 1", "Answer 2"); + Concept textConcept = testConceptService.createConcept("Text Concept", ConceptDataType.Text); + Form locationForm = new TestFormBuilder() + .withUuid(UUID.randomUUID().toString()) + .withFormType(FormType.Location) + .withName("Location Form") + .addFormElementGroup( + new TestFormElementGroupBuilder() + .withDisplayOrder(1d) + .withUuid(UUID.randomUUID().toString()) + .addFormElement( + new TestFormElementBuilder().withDisplayOrder(1d).withType(FormElement.SINGLE_SELECT).withConcept(codedConcept).withName(codedConcept.getName()).withUuid(UUID.randomUUID().toString()).build(), + new TestFormElementBuilder().withDisplayOrder(2d).withConcept(textConcept).withName(textConcept.getName()).withUuid(UUID.randomUUID().toString()).build() + ) + .withName("Location Form Element Group") + .build() + ).build(); + formRepository.save(locationForm); setUser(organisationData.getUser().getUsername()); Map hierarchies = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); @@ -60,17 +83,18 @@ private static int newLocationsCreated(int count) { return count; } - private void lineageExists(String ... lineage) { - AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(".", lineage)).get(); + private void lineageExists(String... lineage) { + AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(", ", lineage)).get(); assertNotNull(address); } - private void locationHasAttribute(String[] lineage, String concept) { - AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(".", lineage)).get(); - assertNotNull(address.getLocationProperties().get(concept)); + private void locationHasAttribute(String[] lineage, String conceptName) { + AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(", ", lineage)).get(); + Concept concept = conceptRepository.findByName(conceptName); + assertNotNull(address.getLocationProperties().get(concept.getUuid())); } - private void treatAsDescriptor(String[] headers, String ... additionalHeaders) { + private void treatAsDescriptor(String[] headers, String... additionalHeaders) { long before = addressLevelRepository.count(); bulkLocationCreator.write(Collections.singletonList(new Row(headers, additionalHeaders)), hierarchy); long after = addressLevelRepository.count(); @@ -88,6 +112,7 @@ private void failure(String[] headers, String[] cells, String errorMessage) { long before = addressLevelRepository.count(); try { bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); + fail(); } catch (Exception e) { assertEquals(errorMessage, e.getMessage()); } @@ -96,7 +121,6 @@ private void failure(String[] headers, String[] cells, String errorMessage) { } @Test - @Ignore public void shouldCreate() { success(header("State", "District", "Block", "GPS coordinates"), dataRow("Bihar", "Vaishali", "Mahua", "23.45,43.85"), @@ -119,18 +143,19 @@ public void shouldCreate() { failure(header("State1", "District", "Block", "GPS coordinates"), dataRow("Bihar", "Vaishali", "Nijma", "23.45,43.85"), - LocationTypesHeaderError); + error(LocationTypesHeaderError)); failure(header("State", "District2", "Block", "GPS coordinates"), dataRow("Bihar", "Vaishali", "Nijma", "23.45,43.85"), - LocationTypesHeaderError); + error(LocationTypesHeaderError)); + failure(header("District", "State", "Block", "GPS coordinates"), + dataRow("Vaishali", "Bihar", "Nijma", "23.45,43.85"), + error(LocationTypesHeaderError)); failure(header("State", "District", "Block", "GPS"), dataRow("Bihar", "Vaishali", "Nijma", "23.45,43.85"), - UnknownHeadersErrorMessage); - - treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), - dataRow("Ex: state 1", "Ex: distr 1", "Ex: blo 1", "Ex. 23.45,43.85")); - treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), - dataRow(" Ex: state 1", "Ex: distr 1 ", "Ex: blo 1", " Ex. 23.45,43.85 ")); + error(UnknownHeadersErrorMessage)); + failure(header("State", "District", "Block", "GPS coordinates"), + dataRow("Bihar", "Vaishali", "Nijma", "23.45,"), + error("Invalid 'GPS coordinates'")); //attributes @@ -151,7 +176,7 @@ public void shouldCreate() { failure(header("State", "District", "Block", "GPS coordinates", "Coded Concept "), dataRow("Bihar", "Vaishali", "Block 4", "23.45,43.86", "not an answer to this concept"), - ""); + error("Invalid answer 'not an answer to this concept' for 'Coded Concept'")); success(header("State", "District", "Block", "GPS coordinates", " Coded Concept", "Text Concept"), dataRow("Bihar", "Vaishali", "Block 5", "23.45,43.86", " Answer 1", "any text"), @@ -178,9 +203,22 @@ public void shouldCreate() { // if in random steps failure(header("State", "District", "Block", "GPS coordinates"), - dataRow(" ", " ", "Block11", "23.45,43.85"), ""); + dataRow(" ", " ", "Block11", "23.45,43.85"), + error(ParentMissingOfLocation)); failure(header("State", "District", "Block", "GPS coordinates"), - dataRow(" ", " District 11", "Block11", "23.45,43.85"), ""); + dataRow(" ", " District 11", "Block11", "23.45,43.85"), + error(ParentMissingOfLocation)); + failure(header("State", "District", "Block", "GPS coordinates"), + dataRow(" ", " ", " ", "23.45,43.85"), + error(NoLocationProvided)); // end + + + treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), + dataRow("Example: state 1", "Ex: distr 1", "Ex: blo 1", "Ex. 23.45,43.85")); + treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), + dataRow(" Example: state 1", "Ex: distr 1 ", "Ex: blo 1", " Ex. 23.45,43.85 ")); + treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), + dataRow(" state 1", "Example: distr 1 ", "Ex: blo 1", " Ex. 23.45,43.85 ")); } } diff --git a/avni-server-api/src/test/java/org/avni/server/service/EnhancedValidationServiceQuestionGroupsTest.java b/avni-server-api/src/test/java/org/avni/server/service/EnhancedValidationServiceQuestionGroupsTest.java index 4f7371ac1..772a7ae30 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/EnhancedValidationServiceQuestionGroupsTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/EnhancedValidationServiceQuestionGroupsTest.java @@ -14,6 +14,7 @@ import org.avni.server.domain.SubjectType; import org.avni.server.domain.factory.metadata.ConceptBuilder; import org.avni.server.domain.factory.metadata.FormMappingBuilder; +import org.avni.server.domain.factory.metadata.TestFormBuilder; import org.avni.server.domain.metadata.SubjectTypeBuilder; import org.avni.server.util.BugsnagReporter; import org.avni.server.web.request.ObservationRequest; diff --git a/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java index 84d648c4d..0449c735c 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java +++ b/avni-server-api/src/test/java/org/avni/server/service/ObservationServiceTest.java @@ -10,6 +10,7 @@ import org.avni.server.domain.ConceptDataType; import org.avni.server.domain.ObservationCollection; import org.avni.server.domain.factory.metadata.ConceptBuilder; +import org.avni.server.domain.factory.metadata.TestFormBuilder; import org.avni.server.web.external.request.export.ExportFilters; import org.avni.server.web.request.ObservationRequest; import org.junit.Before; diff --git a/avni-server-api/src/test/java/org/avni/server/util/CollectionUtilTest.java b/avni-server-api/src/test/java/org/avni/server/util/CollectionUtilTest.java new file mode 100644 index 000000000..d6219aa2f --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/util/CollectionUtilTest.java @@ -0,0 +1,17 @@ +package org.avni.server.util; + +import org.junit.Test; +import java.util.Arrays; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CollectionUtilTest { + @Test + public void hasOnlyTrailingEmptyStrings() { + assertTrue(CollectionUtil.hasOnlyTrailingEmptyStrings(Arrays.asList("a", "b", ""))); + assertTrue(CollectionUtil.hasOnlyTrailingEmptyStrings(Arrays.asList("a", "b", "", ""))); + assertTrue(CollectionUtil.hasOnlyTrailingEmptyStrings(Arrays.asList("a", "b", "c"))); + assertFalse(CollectionUtil.hasOnlyTrailingEmptyStrings(Arrays.asList("a", "", "c"))); + assertFalse(CollectionUtil.hasOnlyTrailingEmptyStrings(Arrays.asList("", "b", "c"))); + } +} From ce65bac44dba32663aac1cf13d115394f4e71f61 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 23 Aug 2024 12:07:12 +0530 Subject: [PATCH 117/129] avniproject/avni-webapp#1306 - initial version of location edit test --- .../domain/factory/AddressLevelBuilder.java | 9 ++- .../BulkLocationEditorIntegrationTest.java | 74 +++++++++++++++++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java index b5cb4e55c..cd57a60a9 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/AddressLevelBuilder.java @@ -2,6 +2,7 @@ import org.avni.server.domain.AddressLevel; import org.avni.server.domain.AddressLevelType; +import org.avni.server.domain.ParentLocationMapping; import java.util.UUID; @@ -23,7 +24,13 @@ public AddressLevelBuilder type(AddressLevelType addressLevelType) { } public AddressLevelBuilder parent(AddressLevel parent) { - entity.setParent(parent); + parent.addChild(entity); + + ParentLocationMapping parentLocationMapping = new ParentLocationMapping(); + parentLocationMapping.assignUUID(); + parentLocationMapping.setParentLocation(parent); + parentLocationMapping.setLocation(entity); + entity.setParentLocationMapping(parentLocationMapping); return this; } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java index 8dde05c5b..8b747aa55 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java @@ -1,18 +1,32 @@ package org.avni.server.importer.batch.csv.writer; +import org.avni.server.application.*; +import org.avni.server.dao.LocationRepository; +import org.avni.server.dao.application.FormRepository; +import org.avni.server.domain.AddressLevel; import org.avni.server.domain.AddressLevelType; +import org.avni.server.domain.Concept; import org.avni.server.domain.ConceptDataType; +import org.avni.server.domain.factory.AddressLevelBuilder; import org.avni.server.domain.factory.AddressLevelTypeBuilder; +import org.avni.server.domain.factory.metadata.TestFormBuilder; +import org.avni.server.importer.batch.model.Row; +import org.avni.server.service.LocationHierarchyService; import org.avni.server.service.builder.TestConceptService; import org.avni.server.service.builder.TestDataSetupService; +import org.avni.server.service.builder.TestLocationService; import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Map; import java.util.UUID; +import static org.junit.Assert.assertNotNull; + @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public class BulkLocationEditorIntegrationTest extends BaseCSVImportTest { @@ -20,18 +34,68 @@ public class BulkLocationEditorIntegrationTest extends BaseCSVImportTest { private TestDataSetupService testDataSetupService; @Autowired private TestConceptService testConceptService; + @Autowired + private FormRepository formRepository; + @Autowired + private LocationHierarchyService locationHierarchyService; + @Autowired + private TestLocationService testLocationService; + @Autowired + private BulkLocationEditor bulkLocationEditor; + private String hierarchy; + @Autowired + private LocationRepository locationRepository; - @Test - @Ignore - public void shouldEdit() { + @Override + public void setUp() throws Exception { TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation(); AddressLevelType block = new AddressLevelTypeBuilder().name("Block").level(2d).withUuid(UUID.randomUUID()).build(); AddressLevelType district = new AddressLevelTypeBuilder().name("District").level(3d).withUuid(UUID.randomUUID()).build(); AddressLevelType state = new AddressLevelTypeBuilder().name("State").level(4d).withUuid(UUID.randomUUID()).build(); testDataSetupService.saveLocationTypes(Arrays.asList(block, district, state)); - testConceptService.createCodedConcept("Coded Concept", "Answer 1", "Answer 2"); - testConceptService.createConcept("Text Concept", ConceptDataType.Text); + Concept codedConcept = testConceptService.createCodedConcept("Coded Concept", "Answer 1", "Answer 2"); + Concept textConcept = testConceptService.createConcept("Text Concept", ConceptDataType.Text); + Form locationForm = new TestFormBuilder() + .withUuid(UUID.randomUUID().toString()) + .withFormType(FormType.Location) + .withName("Location Form") + .addFormElementGroup( + new TestFormElementGroupBuilder() + .withDisplayOrder(1d) + .withUuid(UUID.randomUUID().toString()) + .addFormElement( + new TestFormElementBuilder().withDisplayOrder(1d).withType(FormElement.SINGLE_SELECT).withConcept(codedConcept).withName(codedConcept.getName()).withUuid(UUID.randomUUID().toString()).build(), + new TestFormElementBuilder().withDisplayOrder(2d).withConcept(textConcept).withName(textConcept.getName()).withUuid(UUID.randomUUID().toString()).build() + ) + .withName("Location Form Element Group") + .build() + ).build(); + formRepository.save(locationForm); + + AddressLevel bihar = testLocationService.save(new AddressLevelBuilder().title("Bihar").type(state).withUuid(UUID.randomUUID()).withDefaultValuesForNewEntity().build()); + AddressLevel vaishali = testLocationService.save(new AddressLevelBuilder().title("Vaishali").parent(bihar).type(district).withUuid(UUID.randomUUID()).withDefaultValuesForNewEntity().build()); + testLocationService.save(new AddressLevelBuilder().title("Mahua").parent(vaishali).type(block).withUuid(UUID.randomUUID()).withDefaultValuesForNewEntity().build()); + testLocationService.save(new AddressLevelBuilder().title("Gaya").parent(bihar).type(district).withUuid(UUID.randomUUID()).withDefaultValuesForNewEntity().build()); setUser(organisationData.getUser().getUsername()); + Map hierarchies = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); + hierarchy = String.join(".", hierarchies.keySet()); + } + + private void lineageExists(String... lineage) { + AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(", ", lineage)).get(); + assertNotNull(address); + } + + @Test + @Ignore + public void shouldEdit() { + success(header("Location with full hierarchy","New location name","Parent location with full hierarchy","GPS coordinates"), + dataRow("Bihar, Vaishali, Mahua", "Mahnar", "Bihar, Vaishali", "23.45,43.85")); + lineageExists("Bihar", "Vaishali", "Mahnar"); + } + + private void success(String[] headers, String[] dataRow) { + bulkLocationEditor.editLocation(new Row(headers, dataRow), new ArrayList<>()); } } From a258d260e27ee5c524f9395748782501d0b533c9 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Fri, 23 Aug 2024 14:03:21 +0530 Subject: [PATCH 118/129] avniproject/avni-webapp#1306 - test scenarios for edit location --- .../BulkLocationEditorIntegrationTest.java | 123 +++++++++++++++--- .../src/test/resources/application.properties | 2 +- 2 files changed, 107 insertions(+), 18 deletions(-) diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java index 8b747aa55..909614d38 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java @@ -1,6 +1,7 @@ package org.avni.server.importer.batch.csv.writer; import org.avni.server.application.*; +import org.avni.server.dao.AddressLevelTypeRepository; import org.avni.server.dao.LocationRepository; import org.avni.server.dao.application.FormRepository; import org.avni.server.domain.AddressLevel; @@ -15,17 +16,14 @@ import org.avni.server.service.builder.TestConceptService; import org.avni.server.service.builder.TestDataSetupService; import org.avni.server.service.builder.TestLocationService; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; -import java.util.UUID; +import java.util.*; +import java.util.stream.IntStream; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @@ -72,10 +70,21 @@ public void setUp() throws Exception { ).build(); formRepository.save(locationForm); - AddressLevel bihar = testLocationService.save(new AddressLevelBuilder().title("Bihar").type(state).withUuid(UUID.randomUUID()).withDefaultValuesForNewEntity().build()); - AddressLevel vaishali = testLocationService.save(new AddressLevelBuilder().title("Vaishali").parent(bihar).type(district).withUuid(UUID.randomUUID()).withDefaultValuesForNewEntity().build()); - testLocationService.save(new AddressLevelBuilder().title("Mahua").parent(vaishali).type(block).withUuid(UUID.randomUUID()).withDefaultValuesForNewEntity().build()); - testLocationService.save(new AddressLevelBuilder().title("Gaya").parent(bihar).type(district).withUuid(UUID.randomUUID()).withDefaultValuesForNewEntity().build()); + AddressLevel bihar = testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Bihar").type(state).build()); + AddressLevel district1 = testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("District1").parent(bihar).type(district).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block11").parent(district1).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block12").parent(district1).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block13").parent(district1).type(block).build()); + + AddressLevel district2 = testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("District2").parent(bihar).type(district).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block21").parent(district2).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block22").parent(district2).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block23").parent(district2).type(block).build()); + + AddressLevel district3 = testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("District3").parent(bihar).type(district).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block31").parent(district3).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block32").parent(district3).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block33").parent(district3).type(block).build()); setUser(organisationData.getUser().getUsername()); Map hierarchies = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); @@ -83,19 +92,99 @@ public void setUp() throws Exception { } private void lineageExists(String... lineage) { - AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(", ", lineage)).get(); - assertNotNull(address); + Optional address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(", ", lineage)); + assertTrue(address.isPresent()); + } + + private void lineageNotExists(String... lineage) { + Optional address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(", ", lineage)); + assertFalse(address.isPresent()); + } + + private String[] verifyExists(String... strings) { + return strings; + } + + private String[] verifyNotExists(String... strings) { + return strings; } @Test - @Ignore public void shouldEdit() { - success(header("Location with full hierarchy","New location name","Parent location with full hierarchy","GPS coordinates"), - dataRow("Bihar, Vaishali, Mahua", "Mahnar", "Bihar, Vaishali", "23.45,43.85")); - lineageExists("Bihar", "Vaishali", "Mahnar"); + // no change + success(header("Location with full hierarchy", "New location name", "Parent location with full hierarchy", "GPS coordinates"), + dataRow("Bihar, District1, Block11", "Block11", "Bihar, District1", "23.45,43.85"), + verifyExists("Bihar", "District1", "Block11")); + + // no change with spaces + success(header(" Location with full hierarchy ", " New location name", " Parent location with full hierarchy", " GPS coordinates"), + dataRow("Bihar, District1, Block11", "Block11", "Bihar, District1", "23.45,43.85"), + verifyExists("Bihar", "District1", "Block11")); + + // change name + success(header("Location with full hierarchy", "New location name", "Parent location with full hierarchy", "GPS coordinates"), + dataRow("Bihar, District1, Block11", "Block11toNew", "Bihar, District1", "23.45,43.85"), + verifyExists("Bihar", "District1", "Block11toNew"), + verifyNotExists("Bihar, District1, Block11")); + + // change parent + success(header("Location with full hierarchy", "New location name", "Parent location with full hierarchy", "GPS coordinates"), + dataRow("Bihar, District1, Block12", "Block24", "Bihar, District2", "23.45,43.85"), + verifyExists("Bihar", "District2", "Block24"), + verifyNotExists("Bihar, District1, Block12")); + + // existing location in different case + success(header("Location with full hierarchy", "New location name", "Parent location with full hierarchy", "GPS coordinates"), + dataRow("Bihar, District1, Block13", "Block13new", "bihar, district1", "23.45,43.85"), + verifyExists("Bihar", "District1", "Block13new"), + verifyNotExists("Bihar, District1, Block13")); + + // lineage with spaces + failure(header("Location with full hierarchy", "New location name", "Parent location with full hierarchy", "GPS coordinates"), + dataRow("Bihar, District3, Block31", " Block31New", "Bihar, District3", "23.45,43.85"), + error("Provided Location does not exist in Avni. Please add it or check for spelling mistakes 'Bihar, District3, Block31'"), + verifyExists("Bihar", "District3", "Block31"), + verifyNotExists("Bihar, District3, Block31New")); + + // change to non existing parent + failure(header("Location with full hierarchy", "New location name", "Parent location with full hierarchy", "GPS coordinates"), + dataRow("Bihar, District2, Block21", " Block21Town", "Bihar, DistrictN", "23.45,43.85"), + error("Provided new Location parent does not exist in Avni. Please add it or check for spelling mistakes 'Bihar, DistrictN'"), + verifyExists("Bihar", "District2", "Block21"), + verifyNotExists("Bihar, District2, Block21Town")); + + treatAsDescriptor(header("Location with full hierarchy", "New location name", "Parent location with full hierarchy", "GPS coordinates"), + dataRow("Can be found from Admin -> Locations -> Click Export. Used to specify which location's fields need to be updated. mandatory field", + "Enter new name here ONLY if it needs to be updated", + "Hierarchy of parent location that should contain the child location", + "Ex: 23.45,43.85")); } - private void success(String[] headers, String[] dataRow) { + private void treatAsDescriptor(String[] headers, String... descriptorCells) { + long before = locationRepository.count(); + bulkLocationEditor.write(Collections.singletonList(new Row(headers, descriptorCells))); + long after = locationRepository.count(); + assertEquals(before, after); + } + + private void success(String[] headers, String[] dataRow, String[] exists, String[] ... notExists) { bulkLocationEditor.editLocation(new Row(headers, dataRow), new ArrayList<>()); + lineageExists(exists); + for (String[] notExist : notExists) { + lineageNotExists(notExist); + } + } + + private void failure(String[] headers, String[] dataRow, String errorMessage, String[] exists, String[] ... notExists) { + try { + bulkLocationEditor.editLocation(new Row(headers, dataRow), new ArrayList<>()); + fail(); + } catch (Exception exception) { + assertEquals(errorMessage, exception.getMessage()); + } + lineageExists(exists); + for (String[] notExist : notExists) { + lineageNotExists(notExist); + } } } diff --git a/avni-server-api/src/test/resources/application.properties b/avni-server-api/src/test/resources/application.properties index 14b8e97d4..b08725666 100644 --- a/avni-server-api/src/test/resources/application.properties +++ b/avni-server-api/src/test/resources/application.properties @@ -19,7 +19,7 @@ spring.flyway.schemas=public spring.flyway.baseline-on-migrate=false # JPA, Hibernate and Spring Data -spring.jpa.show-sql=true +spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=none spring.jpa.properties.jadira.usertype.databaseZone=jvm spring.jpa.properties.jadira.usertype.javaZone=jvm From 82c254c9a8917595d88174cb93edecb3b4beb897 Mon Sep 17 00:00:00 2001 From: himeshr Date: Fri, 23 Aug 2024 17:58:25 +0530 Subject: [PATCH 119/129] avniproject/avni-webapp#1292 | Modify duplicate concept error msg --- .../src/main/java/org/avni/server/service/ConceptService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/ConceptService.java b/avni-server-api/src/main/java/org/avni/server/service/ConceptService.java index 24b099e04..3fd28fdfb 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ConceptService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ConceptService.java @@ -176,7 +176,7 @@ private OAE updateOrganisationIfNeeded(@No private Concept saveOrUpdate(ConceptContract conceptRequest) throws AnswerConceptNotFoundException { if (conceptRequest == null) return null; if (conceptExistsWithSameNameAndDifferentUUID(conceptRequest)) { - throw new BadRequestError(String.format("Concept %s exists with different uuid", conceptRequest.getName())); + throw new BadRequestError(String.format("Concept with name \'%s\' already exists", conceptRequest.getName())); } logger.info(String.format("Creating concept: %s", conceptRequest.toString())); From 94d49132597150252b4b308ccdf51fdd04fee76a Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Mon, 26 Aug 2024 14:04:45 +0530 Subject: [PATCH 120/129] avniproject/avni-webapp#1306 - location creator test with verification of lineage created. few more scenarios. handled GeoLocation errors explicitly, instead of logging exception. --- .../csv/creator/BasicEncounterCreator.java | 4 +- .../batch/csv/creator/LocationCreator.java | 21 +++--- .../csv/writer/BulkLocationModifier.java | 2 +- .../csv/writer/ProgramEnrolmentWriter.java | 4 +- .../batch/csv/writer/SubjectWriter.java | 2 +- .../BulkLocationCreatorIntegrationTest.java | 74 +++++++++++++------ .../BulkLocationEditorIntegrationTest.java | 5 -- 7 files changed, 69 insertions(+), 43 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/BasicEncounterCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/BasicEncounterCreator.java index 7fe5d902c..a1b8474ab 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/BasicEncounterCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/BasicEncounterCreator.java @@ -49,8 +49,8 @@ public AbstractEncounter updateEncounter(Row row, AbstractEncounter basicEncount )); if (visitDate != null) basicEncounter.setEncounterDateTime(visitDate.toDateTimeAtStartOfDay(), userService.getCurrentUser()); - basicEncounter.setEncounterLocation(locationCreator.getLocation(row, CommonEncounterHeaders.encounterLocation, allErrorMsgs)); - basicEncounter.setCancelLocation(locationCreator.getLocation(row, CommonEncounterHeaders.cancelLocation, allErrorMsgs)); + basicEncounter.setEncounterLocation(locationCreator.getGeoLocation(row, CommonEncounterHeaders.encounterLocation, allErrorMsgs)); + basicEncounter.setCancelLocation(locationCreator.getGeoLocation(row, CommonEncounterHeaders.cancelLocation, allErrorMsgs)); EncounterType encounterType = encounterTypeCreator.getEncounterType(row.get(CommonEncounterHeaders.encounterTypeHeaderName), CommonEncounterHeaders.encounterTypeHeaderName); basicEncounter.setEncounterType(encounterType); return basicEncounter; diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/LocationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/LocationCreator.java index 2245b221a..ce68267a9 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/LocationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/creator/LocationCreator.java @@ -11,17 +11,20 @@ public class LocationCreator { private static final Logger logger = LoggerFactory.getLogger(LocationCreator.class); - public Point getLocation(Row row, String header, List errorMsgs) { - try { - String location = row.get(header); - if (!S.isEmpty(location)) { - String[] points = location.split(","); + public Point getGeoLocation(Row row, String header, List errorMsgs) { + String location = row.get(header); + if (!S.isEmpty(location)) { + String[] points = location.split(","); + if (points.length != 2) { + errorMsgs.add("Invalid 'GPS coordinates'"); + return null; + } + try { return new Point(Double.parseDouble(points[0].trim()), Double.parseDouble(points[1].trim())); + } catch (NumberFormatException e) { + errorMsgs.add("Invalid 'GPS coordinates'"); + return null; } - } catch (Exception ex) { - logger.error(String.format("Error processing row %s", row), ex); - errorMsgs.add(String.format("Invalid '%s'", header)); - return null; } return null; } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java index 20b0f8a63..f427ea0df 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationModifier.java @@ -22,7 +22,7 @@ public BulkLocationModifier(LocationRepository locationRepository, ObservationCr protected void updateLocationProperties(Row row, List allErrorMsgs, AddressLevel location) { LocationCreator locationCreator = new LocationCreator(); - location.setGpsCoordinates(locationCreator.getLocation(row, LocationHeaders.gpsCoordinates, allErrorMsgs)); + location.setGpsCoordinates(locationCreator.getGeoLocation(row, LocationHeaders.gpsCoordinates, allErrorMsgs)); location.setLocationProperties(observationCreator.getObservations(row, headers, allErrorMsgs, FormType.Location, location.getLocationProperties())); locationRepository.save(location); } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/ProgramEnrolmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/ProgramEnrolmentWriter.java index aaca43832..b78982aef 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/ProgramEnrolmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/ProgramEnrolmentWriter.java @@ -94,8 +94,8 @@ private void write(Row row) throws Exception { if (exitDate != null) programEnrolment.setProgramExitDateTime(exitDate.toDateTimeAtStartOfDay()); LocationCreator locationCreator = new LocationCreator(); - programEnrolment.setEnrolmentLocation(locationCreator.getLocation(row, ProgramEnrolmentHeaders.enrolmentLocation, allErrorMsgs)); - programEnrolment.setExitLocation(locationCreator.getLocation(row, ProgramEnrolmentHeaders.exitLocation, allErrorMsgs)); + programEnrolment.setEnrolmentLocation(locationCreator.getGeoLocation(row, ProgramEnrolmentHeaders.enrolmentLocation, allErrorMsgs)); + programEnrolment.setExitLocation(locationCreator.getGeoLocation(row, ProgramEnrolmentHeaders.exitLocation, allErrorMsgs)); programEnrolment.setProgram(program); FormMapping formMapping = formMappingRepository.getRequiredFormMapping(individual.getSubjectType().getUuid(), program.getUuid(), null, FormType.ProgramEnrolment); if (formMapping == null) { diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/SubjectWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/SubjectWriter.java index a509f2292..09eb8b4bb 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/SubjectWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/SubjectWriter.java @@ -98,7 +98,7 @@ private void write(Row row) throws Exception { individual.setDateOfBirthVerified(row.getBool(SubjectHeaders.dobVerified)); setRegistrationDate(individual, row, allErrorMsgs); LocationCreator locationCreator = new LocationCreator(); - individual.setRegistrationLocation(locationCreator.getLocation(row, SubjectHeaders.registrationLocation, allErrorMsgs)); + individual.setRegistrationLocation(locationCreator.getGeoLocation(row, SubjectHeaders.registrationLocation, allErrorMsgs)); AddressLevelTypes registrationLocationTypes = subjectTypeService.getRegistrableLocationTypes(subjectType); individual.setAddressLevel(addressLevelCreator.findAddressLevel(row, registrationLocationTypes)); diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java index cca4d1939..b2331ade3 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java @@ -17,7 +17,6 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; -import wiremock.org.checkerframework.checker.units.qual.A; import java.util.Arrays; import java.util.Collections; @@ -83,9 +82,10 @@ private static int newLocationsCreated(int count) { return count; } - private void lineageExists(String... lineage) { - AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(String.join(", ", lineage)).get(); - assertNotNull(address); + private void assertlineageExists(String... lineage) { + String titleLineage = String.join(", ", lineage); + AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(titleLineage).get(); + assertNotNull(titleLineage, address); } private void locationHasAttribute(String[] lineage, String conceptName) { @@ -101,11 +101,12 @@ private void treatAsDescriptor(String[] headers, String... additionalHeaders) { assertEquals(before, after); } - private void success(String[] headers, String[] cells, int numberOfNewLocations) { + private void success(String[] headers, String[] cells, int numberOfNewLocations, String[]... lineages) { long before = addressLevelRepository.count(); bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); long after = addressLevelRepository.count(); assertEquals(before + newLocationsCreated(numberOfNewLocations), after); + Arrays.stream(lineages).forEach(this::assertlineageExists); } private void failure(String[] headers, String[] cells, String errorMessage) { @@ -120,26 +121,43 @@ private void failure(String[] headers, String[] cells, String errorMessage) { assertEquals(before, after); } + private String[] lineageExists(String... lineage) { + return lineage; + } + @Test public void shouldCreate() { + // three locations, full lineage created success(header("State", "District", "Block", "GPS coordinates"), dataRow("Bihar", "Vaishali", "Mahua", "23.45,43.85"), - newLocationsCreated(3)); + newLocationsCreated(3), + lineageExists("Bihar", "Vaishali", "Mahua") + ); + + // Location with space success(header("State", "District", "Block", "GPS coordinates"), dataRow(" Bihar", " Vaishali ", " Jamui ", "23.20,43.85"), - newLocationsCreated(1)); + newLocationsCreated(1), + lineageExists("Bihar", "Vaishali", "Jamui") + ); success(header("State", "District", "Block", "GPS coordinates"), dataRow(" Bihar", " Darbhanga "), - newLocationsCreated(1)); + newLocationsCreated(1), + lineageExists("Bihar", "Darbhanga")); success(header("State", "District", "Block", "GPS coordinates"), dataRow(" Bihar", " Aara ", " ", "24.20,43.85"), - newLocationsCreated(1)); + newLocationsCreated(1), + lineageExists("Bihar", "Aara")); success(header(" State ", "District", " Block", " GPS coordinates"), dataRow(" Bihar", " Chapra ", " ", "24.20,43.85"), - newLocationsCreated(1)); + newLocationsCreated(1), + lineageExists("Bihar", "Chapra")); + + // upper case success(header("State", "District", "Block", "GPS coordinates"), dataRow(" bihar", " VAISHALI ", " Tarora ", "23.20,43.85"), - newLocationsCreated(1)); + newLocationsCreated(1), + lineageExists("Bihar", "Vaishali", "Tarora")); failure(header("State1", "District", "Block", "GPS coordinates"), dataRow("Bihar", "Vaishali", "Nijma", "23.45,43.85"), @@ -163,12 +181,13 @@ public void shouldCreate() { dataRow("Bihar", "Vaishali", "Block 1", "23.45,43.86", "Answer 1"), newLocationsCreated(1)); locationHasAttribute(lineage("Bihar", "Vaishali", "Block 1"), "Coded Concept"); - + //attributes with space in header success(header("State", "District", "Block", "GPS coordinates", " Coded Concept"), dataRow("Bihar", "Vaishali", "Block 2", "23.45,43.86", " Answer 1"), newLocationsCreated(1)); locationHasAttribute(lineage("Bihar", "Vaishali", "Block 2"), "Coded Concept"); + //attributes with text concept type success(header("State", "District", "Block", "GPS coordinates", "Text Concept"), dataRow("Bihar", "Vaishali", "Block 3", "23.45,43.86", "any text"), newLocationsCreated(1)); @@ -178,30 +197,30 @@ public void shouldCreate() { dataRow("Bihar", "Vaishali", "Block 4", "23.45,43.86", "not an answer to this concept"), error("Invalid answer 'not an answer to this concept' for 'Coded Concept'")); + // multiple attributes success(header("State", "District", "Block", "GPS coordinates", " Coded Concept", "Text Concept"), dataRow("Bihar", "Vaishali", "Block 5", "23.45,43.86", " Answer 1", "any text"), newLocationsCreated(1)); locationHasAttribute(lineage("Bihar", "Vaishali", "Block 5"), "Coded Concept"); locationHasAttribute(lineage("Bihar", "Vaishali", "Block 5"), "Text Concept"); - // end // without full hierarchy success(header("State", "District", "Block", "GPS coordinates"), - dataRow("Bihar", "District 1", " ", "23.45,43.85"), newLocationsCreated(1)); - lineageExists("Bihar", "District 1"); - + dataRow("Bihar", "District 1", " ", "23.45,43.85"), + newLocationsCreated(1), + lineageExists("Bihar", "District 1")); success(header("State", "District", "Block", "GPS coordinates"), - dataRow("State 2", "District 1", " ", "23.45,43.85"), newLocationsCreated(2)); - lineageExists("State 2", "District 1"); - + dataRow("State 2", "District 1", " ", "23.45,43.85"), + newLocationsCreated(2), + lineageExists("State 2", "District 1")); success(header("State", "District", "Block", "GPS coordinates"), - dataRow("State 3", " ", " ", "23.45,43.85"), newLocationsCreated(1)); - lineageExists("State 3"); - // end + dataRow("State 3", " ", " ", "23.45,43.85"), + newLocationsCreated(1), + lineageExists("State 3")); - // if in random steps + // if done in random steps failure(header("State", "District", "Block", "GPS coordinates"), dataRow(" ", " ", "Block11", "23.45,43.85"), error(ParentMissingOfLocation)); @@ -214,6 +233,15 @@ public void shouldCreate() { // end + // create existing location, results in no new location created + success(header("State", "District", "Block", "GPS coordinates"), + dataRow("Bihar", "Vaishali", "Mahua", "23.45,43.85"), + newLocationsCreated(0)); + success(header("State", "District", "Block", "GPS coordinates"), + dataRow("Bihar", "Vaishali", " Mahua ", "23.45,43.85"), + newLocationsCreated(0)); + + treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), dataRow("Example: state 1", "Ex: distr 1", "Ex: blo 1", "Ex. 23.45,43.85")); treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java index 909614d38..ad9dc0c90 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java @@ -35,12 +35,9 @@ public class BulkLocationEditorIntegrationTest extends BaseCSVImportTest { @Autowired private FormRepository formRepository; @Autowired - private LocationHierarchyService locationHierarchyService; - @Autowired private TestLocationService testLocationService; @Autowired private BulkLocationEditor bulkLocationEditor; - private String hierarchy; @Autowired private LocationRepository locationRepository; @@ -87,8 +84,6 @@ public void setUp() throws Exception { testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block33").parent(district3).type(block).build()); setUser(organisationData.getUser().getUsername()); - Map hierarchies = locationHierarchyService.determineAddressHierarchiesForAllAddressLevelTypesInOrg(); - hierarchy = String.join(".", hierarchies.keySet()); } private void lineageExists(String... lineage) { From 051dce8562dc6152dece5d0313280d261f761293 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Tue, 27 Aug 2024 15:56:09 +0530 Subject: [PATCH 121/129] avniproject/avni-webapp#1306 - added guava for set operations used in headers in csv mismatch. made headers mismatch message specific to the missing headers. data setup for integration test, now creates basic org data. first user and catchment test. do not delete data created by migration in setup sql scripts for test. --- avni-server-api/build.gradle | 1 + .../service/MessageTemplateService.java | 5 +- .../batch/csv/writer/BulkLocationEditor.java | 2 + .../csv/writer/UserAndCatchmentWriter.java | 41 ++++--- .../resources/db/migration/R__Functions.sql | 102 ++++++++-------- ...1_343__User1ShouldHaveNoOrganisationId.sql | 2 + .../api/MessageTemplateControllerTest.java | 6 +- .../factory/TestOrganisationBuilder.java | 7 +- .../BulkLocationCreatorIntegrationTest.java | 4 +- ...UserAndCatchmentWriterIntegrationTest.java | 110 ++++++++++++++++++ .../service/builder/TestDataSetupService.java | 22 ++-- .../builder/TestOrganisationService.java | 17 ++- .../server/web/TestWebContextService.java | 6 +- .../src/test/resources/tear-down.sql | 13 ++- .../test-data-openchs-organisation.sql | 15 +-- .../src/test/resources/test-data.sql | 14 +-- 16 files changed, 255 insertions(+), 112 deletions(-) create mode 100644 avni-server-api/src/main/resources/db/migration/V1_343__User1ShouldHaveNoOrganisationId.sql create mode 100644 avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriterIntegrationTest.java diff --git a/avni-server-api/build.gradle b/avni-server-api/build.gradle index fd6f03335..aedf8c3fd 100644 --- a/avni-server-api/build.gradle +++ b/avni-server-api/build.gradle @@ -91,6 +91,7 @@ dependencies { implementation 'org.apache.commons:commons-csv:1.10.0' compile 'com.googlecode.libphonenumber:libphonenumber:8.12.32' testImplementation 'org.slf4j:slf4j-reload4j:2.0.6' + compile 'com.google.guava:guava:33.2.1-jre' } bootRun { diff --git a/avni-server-api/src/main/java/org/avni/messaging/service/MessageTemplateService.java b/avni-server-api/src/main/java/org/avni/messaging/service/MessageTemplateService.java index 4c91e3143..91de2cbbf 100644 --- a/avni-server-api/src/main/java/org/avni/messaging/service/MessageTemplateService.java +++ b/avni-server-api/src/main/java/org/avni/messaging/service/MessageTemplateService.java @@ -13,9 +13,8 @@ @Service public class MessageTemplateService { - - private GlificMessageTemplateRepository messageTemplateRepository; - private OrganisationConfigService organisationConfigService; + private final GlificMessageTemplateRepository messageTemplateRepository; + private final OrganisationConfigService organisationConfigService; @Autowired public MessageTemplateService(GlificMessageTemplateRepository messageTemplateRepository, OrganisationConfigService organisationConfigService) { diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java index 6582ac37c..2b236a89f 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditor.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import javax.transaction.Transactional; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -70,6 +71,7 @@ private void updateExistingLocation(AddressLevel location, AddressLevel newParen updateLocationProperties(row, allErrorMsgs, location); } + @Transactional(Transactional.TxType.REQUIRES_NEW) public void write(List rows) { List allErrorMsgs = new ArrayList<>(); validateEditModeHeaders(rows.get(0).getHeaders(), allErrorMsgs); diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index 32b489e94..74be9a2f9 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -1,5 +1,7 @@ package org.avni.server.importer.batch.csv.writer; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import org.avni.server.dao.LocationRepository; import org.avni.server.dao.UserRepository; import org.avni.server.domain.Locale; @@ -17,10 +19,12 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import javax.transaction.Transactional; import java.io.Serializable; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static java.lang.String.format; import static org.avni.server.domain.OperatingIndividualScope.ByCatchment; @@ -44,8 +48,8 @@ public class UserAndCatchmentWriter implements ItemWriter, Serializable { private static final String ERR_MSG_LOCATION_FIELD = "Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'"; private static final String ERR_MSG_LOCALE_FIELD = "Provided value '%s' for Preferred Language is invalid"; private static final String ERR_MSG_DATE_PICKER_FIELD = "Provided value '%s' for Date picker mode is invalid"; - private static final String ERR_MSG_UNKNOWN_HEADERS = "Unknown headers included in file. Please refer to sample file for valid list of headers"; - private static final String ERR_MSG_MISSING_MANDATORY_FIELDS = "Mandatory columns are missing from uploaded file. Please refer to sample file for the list of mandatory headers."; + private static final String ERR_MSG_UNKNOWN_HEADERS = "Unknown headers - %s included in file. Please refer to sample file for valid list of headers"; + private static final String ERR_MSG_MISSING_MANDATORY_FIELDS = "Mandatory columns are missing from uploaded file - %s. Please refer to sample file for the list of mandatory headers."; private static final String ERR_MSG_INVALID_CONCEPT_ANSWER = "'%s' is not a valid value for the concept '%s'" + "To input this value, add this as an answer to the coded concept '%s'"; private static final String METADATA_ROW_START_STRING = "Mandatory field."; @@ -71,9 +75,10 @@ public UserAndCatchmentWriter(CatchmentService catchmentService, this.compoundHeaderPattern = Pattern.compile("^(?.*?)->(?.*)$"); } + @Transactional(Transactional.TxType.REQUIRES_NEW) @Override - public void write(List rows) throws Exception { - if(!CollectionUtils.isEmpty(rows)) { + public void write(List rows) throws IDPException { + if (!CollectionUtils.isEmpty(rows)) { validateHeaders(rows.get(0).getHeaders()); for (Row row : rows) write(row); } @@ -87,27 +92,33 @@ private void validateHeaders(String[] headers) { List syncAttributeHeadersForSubjectTypes = subjectTypeService.constructSyncAttributeHeadersForSubjectTypes(); checkForMissingHeaders(headerList, allErrorMsgs, expectedStandardHeaders, syncAttributeHeadersForSubjectTypes); checkForUnknownHeaders(headerList, allErrorMsgs, expectedStandardHeaders, syncAttributeHeadersForSubjectTypes); - if(!allErrorMsgs.isEmpty()) { + if (!allErrorMsgs.isEmpty()) { throw new RuntimeException(String.join(ERR_MSG_DELIMITER, allErrorMsgs)); } } private void checkForUnknownHeaders(List headerList, List allErrorMsgs, List expectedStandardHeaders, List syncAttributeHeadersForSubjectTypes) { headerList.removeIf(StringUtils::isEmpty); - headerList.removeIf(header -> expectedStandardHeaders.contains(header)); - headerList.removeIf(header -> syncAttributeHeadersForSubjectTypes.contains(header)); - if (!headerList.isEmpty()) { - allErrorMsgs.add(ERR_MSG_UNKNOWN_HEADERS); + HashSet expectedHeaders = new HashSet<>(expectedStandardHeaders); + expectedHeaders.addAll(syncAttributeHeadersForSubjectTypes); + Sets.SetView unknownHeaders = Sets.difference(new HashSet<>(headerList), expectedHeaders); + if (!unknownHeaders.isEmpty()) { + allErrorMsgs.add(String.format(ERR_MSG_UNKNOWN_HEADERS, String.join(", ", unknownHeaders))); } } private void checkForMissingHeaders(List headerList, List allErrorMsgs, List expectedStandardHeaders, List syncAttributeHeadersForSubjectTypes) { - if (headerList.isEmpty() || !headerList.containsAll(expectedStandardHeaders) || !headerList.containsAll(syncAttributeHeadersForSubjectTypes)) { - allErrorMsgs.add(ERR_MSG_MISSING_MANDATORY_FIELDS); + HashSet expectedHeaders = new HashSet<>(expectedStandardHeaders); + expectedHeaders.addAll(syncAttributeHeadersForSubjectTypes); + HashSet presentHeaders = new HashSet<>(headerList); + presentHeaders.addAll(syncAttributeHeadersForSubjectTypes); + Sets.SetView missingHeaders = Sets.difference(expectedHeaders, presentHeaders); + if (!missingHeaders.isEmpty()) { + allErrorMsgs.add(String.format(ERR_MSG_MISSING_MANDATORY_FIELDS, String.join(", ", missingHeaders))); } } - private void write(Row row) throws Exception { + private void write(Row row) throws IDPException { List rowValidationErrorMsgs = new ArrayList<>(); String fullAddress = row.get(LOCATION_WITH_FULL_HIERARCHY); if (fullAddress != null && fullAddress.startsWith(METADATA_ROW_START_STRING)) return; @@ -179,7 +190,7 @@ private void validateRowAndAssimilateErrors(List rowValidationErrorMsgs, extractUserUsernameValidationErrMsg(rowValidationErrorMsgs, username, userSuffix); extractUserNameValidationErrMsg(rowValidationErrorMsgs, nameOfUser); extractUserEmailValidationErrMsg(rowValidationErrorMsgs, email); - if(!rowValidationErrorMsgs.isEmpty()) { + if (!rowValidationErrorMsgs.isEmpty()) { throw new RuntimeException(String.join(ERR_MSG_DELIMITER, rowValidationErrorMsgs)); } } @@ -209,7 +220,7 @@ private void extractUserUsernameValidationErrMsg(List rowValidationError } private void addErrMsgIfValidationFails(boolean validationCheckResult, List rowValidationErrorMsgs, String validationErrorMessage) { - if(validationCheckResult) { + if (validationCheckResult) { rowValidationErrorMsgs.add(validationErrorMessage); } } @@ -273,7 +284,7 @@ private List findSyncSettingCodedConceptValues(List syncSettings List syncSettingCodedConceptValues = new ArrayList<>(); for (String syncSettingsValue : syncSettingsValues) { Optional conceptAnswer = Optional.ofNullable(conceptService.getByName(syncSettingsValue)); - if(conceptAnswer.isPresent()) { + if (conceptAnswer.isPresent()) { syncSettingCodedConceptValues.add(conceptAnswer.get().getUuid()); } else { rowValidationErrorMsgs.add(format(ERR_MSG_INVALID_CONCEPT_ANSWER, syncSettingsValue, concept.getName(), concept.getName())); diff --git a/avni-server-api/src/main/resources/db/migration/R__Functions.sql b/avni-server-api/src/main/resources/db/migration/R__Functions.sql index c5dc431dc..a85973026 100644 --- a/avni-server-api/src/main/resources/db/migration/R__Functions.sql +++ b/avni-server-api/src/main/resources/db/migration/R__Functions.sql @@ -10,18 +10,18 @@ $BODY$ EXECUTE 'GRANT ' || quote_ident(inrolname) || ' TO openchs'; PERFORM grant_all_on_all(inrolname); RETURN 1; - END +END $BODY$ LANGUAGE PLPGSQL; DROP FUNCTION IF EXISTS create_implementation_schema(text); CREATE OR REPLACE FUNCTION create_implementation_schema(schema_name text, db_user text) - RETURNS BIGINT AS + RETURNS BIGINT AS $BODY$ BEGIN - EXECUTE 'CREATE SCHEMA IF NOT EXISTS "' || schema_name || '" AUTHORIZATION "' || db_user || '"'; - EXECUTE 'GRANT ALL PRIVILEGES ON SCHEMA "' || schema_name || '" TO "' || db_user || '"'; - RETURN 1; + EXECUTE 'CREATE SCHEMA IF NOT EXISTS "' || schema_name || '" AUTHORIZATION "' || db_user || '"'; + EXECUTE 'GRANT ALL PRIVILEGES ON SCHEMA "' || schema_name || '" TO "' || db_user || '"'; + RETURN 1; END $BODY$ LANGUAGE PLPGSQL; @@ -29,20 +29,20 @@ $BODY$ LANGUAGE PLPGSQL; CREATE OR REPLACE FUNCTION jsonb_object_values_contain(obs JSONB, pattern TEXT) RETURNS BOOLEAN AS $$ BEGIN - return EXISTS (select true from jsonb_each_text(obs) where value ilike pattern); + return EXISTS (select true from jsonb_each_text(obs) where value ilike pattern); END; $$ -LANGUAGE plpgsql IMMUTABLE; + LANGUAGE plpgsql IMMUTABLE; CREATE OR REPLACE FUNCTION create_audit(user_id NUMERIC) RETURNS INTEGER AS $$ DECLARE result INTEGER; BEGIN - INSERT INTO audit(created_by_id, last_modified_by_id, created_date_time, last_modified_date_time) + INSERT INTO audit(created_by_id, last_modified_by_id, created_date_time, last_modified_date_time) VALUES(user_id, user_id, now(), now()) RETURNING id into result; - RETURN result; + RETURN result; END $$ - LANGUAGE plpgsql; + LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION create_audit() RETURNS INTEGER AS 'select create_audit(1)' language sql; @@ -58,7 +58,7 @@ CREATE OR REPLACE FUNCTION create_view(schema_name text, view_name text, sql_que RETURNS BIGINT AS $BODY$ BEGIN --- EXECUTE 'set search_path = ' || ; + -- EXECUTE 'set search_path = ' || ; EXECUTE 'DROP VIEW IF EXISTS ' || schema_name || '.' || view_name; EXECUTE 'CREATE OR REPLACE VIEW ' || schema_name || '.' || view_name || ' AS ' || sql_query; RETURN 1; @@ -207,13 +207,13 @@ $body$ BEGIN EXECUTE ( SELECT 'GRANT ALL ON TABLE ' - || tablename + || tablename || ' TO ' || quote_ident(rolename) ); EXECUTE ( SELECT 'GRANT SELECT ON ' - || tablename + || tablename || ' TO ' || quote_ident(rolename) ); @@ -228,19 +228,19 @@ $body$ BEGIN EXECUTE ( SELECT 'GRANT ALL ON TABLE ' - || string_agg(format('%I.%I', table_schema, table_name), ',') - || ' TO ' || quote_ident(rolename) || '' - FROM information_schema.tables - WHERE table_schema = 'public' + || string_agg(format('%I.%I', table_schema, table_name), ',') + || ' TO ' || quote_ident(rolename) || '' + FROM information_schema.tables + WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ); EXECUTE ( SELECT 'GRANT SELECT ON ' - || string_agg(format('%I.%I', schemaname, viewname), ',') - || ' TO ' || quote_ident(rolename) || '' - FROM pg_catalog.pg_views - WHERE schemaname = 'public' + || string_agg(format('%I.%I', schemaname, viewname), ',') + || ' TO ' || quote_ident(rolename) || '' + FROM pg_catalog.pg_views + WHERE schemaname = 'public' and viewowner in ('openchs') ); @@ -276,21 +276,21 @@ CREATE OR REPLACE FUNCTION multi_select_coded(obs JSONB) AS $$ DECLARE result VARCHAR; BEGIN - BEGIN - IF JSONB_TYPEOF(obs) = 'array' - THEN + BEGIN + IF JSONB_TYPEOF(obs) = 'array' + THEN SELECT STRING_AGG(C.NAME, ' ,') FROM JSONB_ARRAY_ELEMENTS_TEXT(obs) AS OB (UUID) - JOIN CONCEPT C ON C.UUID = OB.UUID - INTO RESULT; - ELSE - SELECT SINGLE_SELECT_CODED(obs) INTO RESULT; - END IF; - RETURN RESULT; + JOIN CONCEPT C ON C.UUID = OB.UUID + INTO RESULT; + ELSE + SELECT SINGLE_SELECT_CODED(obs) INTO RESULT; + END IF; + RETURN RESULT; EXCEPTION WHEN OTHERS - THEN - RAISE NOTICE 'Failed while processing multi_select_coded(''%'')', obs :: TEXT; - RAISE NOTICE '% %', SQLERRM, SQLSTATE; - END; + THEN + RAISE NOTICE 'Failed while processing multi_select_coded(''%'')', obs :: TEXT; + RAISE NOTICE '% %', SQLERRM, SQLSTATE; + END; END $$; CREATE OR REPLACE FUNCTION single_select_coded(obs TEXT) @@ -298,34 +298,34 @@ CREATE OR REPLACE FUNCTION single_select_coded(obs TEXT) AS $$ DECLARE result VARCHAR; BEGIN - BEGIN + BEGIN SELECT name FROM concept WHERE uuid = obs - INTO result; - RETURN result; - END; + INTO result; + RETURN result; + END; END $$ - STABLE; + STABLE; CREATE OR REPLACE FUNCTION single_select_coded(obs JSONB) RETURNS VARCHAR LANGUAGE plpgsql AS $$ DECLARE result VARCHAR; BEGIN - BEGIN - IF JSONB_TYPEOF(obs) = 'array' - THEN - SELECT name FROM concept WHERE (obs->>0) = uuid INTO result; - ELSEIF JSONB_TYPEOF(obs) = 'string' - THEN - select name from concept where (array_to_json(array[obs])->>0) = uuid into result; - END IF; - RETURN result; - END; + BEGIN + IF JSONB_TYPEOF(obs) = 'array' + THEN + SELECT name FROM concept WHERE (obs ->> 0) = uuid INTO result; + ELSEIF JSONB_TYPEOF(obs) = 'string' + THEN + select name from concept where (array_to_json(array [obs]) ->> 0) = uuid into result; + END IF; + RETURN result; + END; END $$ - STABLE; + STABLE; CREATE OR REPLACE FUNCTION no_op() RETURNS trigger AS $$ BEGIN RETURN NULL; END; -$$ LANGUAGE plpgsql; \ No newline at end of file +$$ LANGUAGE plpgsql; diff --git a/avni-server-api/src/main/resources/db/migration/V1_343__User1ShouldHaveNoOrganisationId.sql b/avni-server-api/src/main/resources/db/migration/V1_343__User1ShouldHaveNoOrganisationId.sql new file mode 100644 index 000000000..a4b873598 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_343__User1ShouldHaveNoOrganisationId.sql @@ -0,0 +1,2 @@ +-- In earlier migration we have set parent_organisation as 1 but fix in production +update users set organisation_id = null where id = 1 and username = 'admin'; diff --git a/avni-server-api/src/test/java/org/avni/messaging/api/MessageTemplateControllerTest.java b/avni-server-api/src/test/java/org/avni/messaging/api/MessageTemplateControllerTest.java index ad32ea28d..22e311a1e 100644 --- a/avni-server-api/src/test/java/org/avni/messaging/api/MessageTemplateControllerTest.java +++ b/avni-server-api/src/test/java/org/avni/messaging/api/MessageTemplateControllerTest.java @@ -4,10 +4,7 @@ import com.github.tomakehurst.wiremock.client.WireMock; import org.avni.messaging.contract.MessageTemplateContract; import org.avni.server.common.AbstractControllerIntegrationTest; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.*; import org.springframework.test.context.jdbc.Sql; import java.util.List; @@ -16,6 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; @Sql(scripts = {"/test-data.sql"}) +@Ignore // this is not testing much and is setup with admin user which is incorrect public class MessageTemplateControllerTest extends AbstractControllerIntegrationTest { private String SAMPLE_AUTH_RESPONSE = "{\"data\":{\"access_token\":\"SFMyNTY.YjQ2M2MzMmMtNGZlOC00OTEyLWIzYTEtZmRhZTRkOGQ1ZTIx.3TjKqpElrD5N2ffGHEAFX91cyp7zwoTztYR8p1jwwgA\",\"renewal_token\":\"SFMyNTY.MjYxODllMTgtNDM1OC00YjJjLTlmN2MtOTA5MzMwYzM3ZjA2.dDigSwftcGFGHu4o9MwkASp2KqH6eitp1aRmeYSgi5M\",\"token_expiry_time\":\"2022-10-13T21:42:33.342529Z\"}}"; diff --git a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java index 6729f5939..b7a26058e 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/factory/TestOrganisationBuilder.java @@ -12,7 +12,7 @@ public class TestOrganisationBuilder { public TestOrganisationBuilder withMandatoryFields() { String placeholder = UUID.randomUUID().toString(); - return withUuid(placeholder).withDbUser("testDbUser").withName(placeholder).withSchemaName(placeholder); + return withUuid(placeholder).withDbUser("testDbUserNew").withName(placeholder).withSchemaName(placeholder); } public TestOrganisationBuilder setId(long id) { @@ -20,6 +20,11 @@ public TestOrganisationBuilder setId(long id) { return this; } + public TestOrganisationBuilder withUsernameSuffix(String usernameSuffix) { + organisation.setUsernameSuffix(usernameSuffix); + return this; + } + public TestOrganisationBuilder withSchemaName(String schemaName) { organisation.setSchemaName(schemaName); return this; diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java index b2331ade3..8dd8ce855 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java @@ -82,7 +82,7 @@ private static int newLocationsCreated(int count) { return count; } - private void assertlineageExists(String... lineage) { + private void assertLineageExists(String... lineage) { String titleLineage = String.join(", ", lineage); AddressLevel address = this.locationRepository.findByTitleLineageIgnoreCase(titleLineage).get(); assertNotNull(titleLineage, address); @@ -106,7 +106,7 @@ private void success(String[] headers, String[] cells, int numberOfNewLocations, bulkLocationCreator.write(Collections.singletonList(new Row(headers, cells)), hierarchy); long after = addressLevelRepository.count(); assertEquals(before + newLocationsCreated(numberOfNewLocations), after); - Arrays.stream(lineages).forEach(this::assertlineageExists); + Arrays.stream(lineages).forEach(this::assertLineageExists); } private void failure(String[] headers, String[] cells, String errorMessage) { diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriterIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriterIntegrationTest.java new file mode 100644 index 000000000..7986a3d55 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriterIntegrationTest.java @@ -0,0 +1,110 @@ +package org.avni.server.importer.batch.csv.writer; + +import org.avni.server.application.*; +import org.avni.server.dao.CatchmentRepository; +import org.avni.server.dao.LocationRepository; +import org.avni.server.dao.UserRepository; +import org.avni.server.dao.application.FormRepository; +import org.avni.server.domain.*; +import org.avni.server.domain.factory.AddressLevelBuilder; +import org.avni.server.domain.factory.AddressLevelTypeBuilder; +import org.avni.server.domain.factory.metadata.TestFormBuilder; +import org.avni.server.domain.metadata.SubjectTypeBuilder; +import org.avni.server.importer.batch.model.Row; +import org.avni.server.service.IDPException; +import org.avni.server.service.builder.TestConceptService; +import org.avni.server.service.builder.TestDataSetupService; +import org.avni.server.service.builder.TestLocationService; +import org.avni.server.service.builder.TestSubjectTypeService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; + +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +public class UserAndCatchmentWriterIntegrationTest extends BaseCSVImportTest { + @Autowired + private TestDataSetupService testDataSetupService; + @Autowired + private TestConceptService testConceptService; + @Autowired + private FormRepository formRepository; + @Autowired + private TestLocationService testLocationService; + @Autowired + private UserAndCatchmentWriter userAndCatchmentWriter; + @Autowired + private TestSubjectTypeService testSubjectTypeService; + @Autowired + private UserRepository userRepository; + @Autowired + private CatchmentRepository catchmentRepository; + + @Override + public void setUp() { + TestDataSetupService.TestOrganisationData organisationData = testDataSetupService.setupOrganisation("example", "User Group 1"); + AddressLevelType block = new AddressLevelTypeBuilder().name("Block").level(2d).withUuid(UUID.randomUUID()).build(); + AddressLevelType district = new AddressLevelTypeBuilder().name("District").level(3d).withUuid(UUID.randomUUID()).build(); + AddressLevelType state = new AddressLevelTypeBuilder().name("State").level(4d).withUuid(UUID.randomUUID()).build(); + testDataSetupService.saveLocationTypes(Arrays.asList(block, district, state)); + Concept codedConcept = testConceptService.createCodedConcept("Sync Concept", "Answer 1", "Answer 2"); + Concept textConcept = testConceptService.createConcept("Text Concept", ConceptDataType.Text); + + testSubjectTypeService.createWithDefaults( + new SubjectTypeBuilder() + .setMandatoryFieldsForNewEntity() + .setUuid(UUID.randomUUID().toString()) + .setName("SubjectTypeWithSyncAttributeBasedSync") + .setSyncRegistrationConcept1Usable(true) + .setSyncRegistrationConcept1(codedConcept.getUuid()).build()); + + AddressLevel bihar = testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Bihar").type(state).build()); + AddressLevel district1 = testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("District1").parent(bihar).type(district).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block11").parent(district1).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block12").parent(district1).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block13").parent(district1).type(block).build()); + + AddressLevel district2 = testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("District2").parent(bihar).type(district).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block21").parent(district2).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block22").parent(district2).type(block).build()); + testLocationService.save(new AddressLevelBuilder().withDefaultValuesForNewEntity().title("Block23").parent(district2).type(block).build()); + } + + private boolean catchmentCreated(boolean b) { + return b; + } + + private boolean userCreated(boolean b) { + return b; + } + + private void success(String[] headers, String[] cells, boolean catchmentCreated, boolean userCreated) throws IDPException { + long numberOfUsers = userRepository.count(); + long numberOfCatchments = catchmentRepository.count(); + userAndCatchmentWriter.write(Collections.singletonList(new Row(headers, cells))); + if (catchmentCreated) + assertEquals(catchmentRepository.count(), numberOfCatchments + 1); + else + assertEquals(catchmentRepository.count(), numberOfCatchments); + if (userCreated) + assertEquals(userRepository.count(), numberOfUsers + 1); + else + assertEquals(userRepository.count(), numberOfUsers); + } + + @Test + public void shouldCreateUpdate() throws IDPException { + // new catchment, new user + success( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", "Catchment 1", "username1@example", "User 1", "username1@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1", "Answer 1"), + catchmentCreated(true), + userCreated(true)); + } +} diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java index fa6a18936..a03cb6395 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestDataSetupService.java @@ -45,21 +45,25 @@ public class TestDataSetupService { @Autowired private OrganisationStatusRepository organisationStatusRepository; - public TestOrganisationData setupOrganisation(String orgSuffix) { - Group group = new TestGroupBuilder().withMandatoryFieldsForNewEntity().build(); - User user1 = new UserBuilder().withDefaultValuesForNewEntity().userName(String.format("user@%s", orgSuffix)).withAuditUser(userRepository.getDefaultSuperAdmin()).build(); - User user2 = new UserBuilder().withDefaultValuesForNewEntity().userName(String.format("user2@%s", orgSuffix)).withAuditUser(userRepository.getDefaultSuperAdmin()).build(); + public TestOrganisationData setupOrganisation(String orgSuffix, String userGroupName) { + User defaultSuperAdmin = userRepository.getDefaultSuperAdmin(); + testWebContextService.setUser(defaultSuperAdmin); + User user1 = new UserBuilder().withDefaultValuesForNewEntity().userName(String.format("user@%s", orgSuffix)).withAuditUser(defaultSuperAdmin).build(); Organisation organisation = new TestOrganisationBuilder() + .withUsernameSuffix(orgSuffix) .setCategory(organisationCategoryRepository.findEntity(1L)) .withStatus(organisationStatusRepository.findEntity(1L)) .withMandatoryFields() .withAccount(accountRepository.getDefaultAccount()).build(); testOrganisationService.createOrganisation(organisation, user1); - testOrganisationService.createUser(organisation, user2); - userRepository.save(new UserBuilder(user1).withAuditUser(user1).build()); - userRepository.save(new UserBuilder(user2).withAuditUser(user1).build()); + testWebContextService.setUser(user1.getUsername()); + + Group group = new TestGroupBuilder().withMandatoryFieldsForNewEntity().withName(userGroupName).build(); + User user2 = new UserBuilder().withDefaultValuesForNewEntity().userName(String.format("user2@%s", orgSuffix)).withAuditUser(user1).build(); + testOrganisationService.createUser(organisation, user2); + userRepository.save(new UserBuilder(user2).withAuditUser(user1).build()); organisationConfigRepository.save(new TestOrganisationConfigBuilder().withMandatoryFields().withOrganisationId(organisation.getId()).build()); groupRepository.save(group); @@ -70,6 +74,10 @@ public TestOrganisationData setupOrganisation(String orgSuffix) { return testOrganisationData; } + public TestOrganisationData setupOrganisation(String orgSuffix) { + return this.setupOrganisation(orgSuffix, UUID.randomUUID().toString()); + } + public TestOrganisationData setupOrganisation() { return this.setupOrganisation("example"); } diff --git a/avni-server-api/src/test/java/org/avni/server/service/builder/TestOrganisationService.java b/avni-server-api/src/test/java/org/avni/server/service/builder/TestOrganisationService.java index bb97fb909..f21ccf132 100644 --- a/avni-server-api/src/test/java/org/avni/server/service/builder/TestOrganisationService.java +++ b/avni-server-api/src/test/java/org/avni/server/service/builder/TestOrganisationService.java @@ -3,6 +3,9 @@ import org.avni.server.dao.*; import org.avni.server.domain.Organisation; import org.avni.server.domain.User; +import org.avni.server.framework.security.UserContextHolder; +import org.avni.server.service.OrganisationService; +import org.avni.server.web.TestWebContextService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -11,22 +14,28 @@ public class TestOrganisationService { private final ImplementationRepository implementationRepository; private final OrganisationRepository organisationRepository; private final UserRepository userRepository; + private final TestWebContextService testWebContextService; + private final OrganisationService organisationService; @Autowired - public TestOrganisationService(ImplementationRepository implementationRepository, OrganisationRepository organisationRepository, UserRepository userRepository) { + public TestOrganisationService(ImplementationRepository implementationRepository, OrganisationRepository organisationRepository, UserRepository userRepository, TestWebContextService testWebContextService, OrganisationService organisationService) { this.implementationRepository = implementationRepository; this.organisationRepository = organisationRepository; this.userRepository = userRepository; + this.testWebContextService = testWebContextService; + this.organisationService = organisationService; } public void createOrganisation(Organisation organisation, User adminUser) { organisationRepository.save(organisation); - createUser(organisation, adminUser); + User orgUser = createUser(organisation, adminUser); implementationRepository.createDBUser(organisation); + testWebContextService.setUser(orgUser); + organisationService.setupBaseOrganisationMetadata(organisation); } - public void createUser(Organisation organisation, User user) { + public User createUser(Organisation organisation, User user) { user.setOrganisationId(organisation.getId()); - userRepository.save(user); + return userRepository.save(user); } } diff --git a/avni-server-api/src/test/java/org/avni/server/web/TestWebContextService.java b/avni-server-api/src/test/java/org/avni/server/web/TestWebContextService.java index f4c5a03d9..3d40e1123 100644 --- a/avni-server-api/src/test/java/org/avni/server/web/TestWebContextService.java +++ b/avni-server-api/src/test/java/org/avni/server/web/TestWebContextService.java @@ -2,6 +2,7 @@ import org.avni.server.dao.OrganisationRepository; import org.avni.server.dao.UserRepository; +import org.avni.server.domain.Organisation; import org.avni.server.domain.User; import org.avni.server.domain.UserContext; import org.avni.server.framework.security.UserContextHolder; @@ -40,7 +41,10 @@ public void setUser(User user) { userContext.setUser(user); UserContextHolder.create(userContext); - userContext.setOrganisation(organisationRepository.findOne(user.getOrganisationId())); + if (user.getOrganisationId() != null) { + Organisation organisation = organisationRepository.findOne(user.getOrganisationId()); + userContext.setOrganisation(organisation); + } SimpleGrantedAuthority[] authorities = Stream.of(USER_AUTHORITY) .filter(authority -> userContext.getRoles().contains(authority.getAuthority())) .toArray(SimpleGrantedAuthority[]::new); diff --git a/avni-server-api/src/test/resources/tear-down.sql b/avni-server-api/src/test/resources/tear-down.sql index cbe5c0aa2..f4f88a9e2 100644 --- a/avni-server-api/src/test/resources/tear-down.sql +++ b/avni-server-api/src/test/resources/tear-down.sql @@ -30,7 +30,8 @@ DELETE FROM location_location_mapping where 1 = 1; DELETE FROM address_level where 1 = 1; DELETE FROM address_level_type where 1 = 1; DELETE FROM catchment where 1 = 1; -DELETE FROM account_admin where 1 = 1; +DELETE FROM account_admin + where admin_id <> (select id from users where username = 'admin'); DELETE FROM user_group where 1 = 1; DELETE FROM external_system_config where 1 = 1; DELETE FROM organisation_config where 1 = 1; @@ -40,12 +41,18 @@ delete from message_rule where 1 = 1; delete from identifier_user_assignment where 1 = 1; delete from identifier_source where 1 = 1; DELETE FROM reset_sync where 1 = 1; -DELETE FROM users where id <> 1; DELETE FROM operational_subject_type where 1 = 1; DELETE FROM subject_type where 1 = 1; delete from group_role where 1 = 1; +delete from dashboard_section_card_mapping where 1 = 1; +delete from report_card where 1 = 1; +delete from group_dashboard where 1 = 1; +delete from dashboard_section where 1 = 1; +delete from dashboard_filter where 1 = 1; +delete from dashboard where 1 = 1; DELETE FROM groups where 1 = 1; -DELETE FROM organisation where id <> 1; +DELETE FROM users where username <> 'admin'; +DELETE FROM organisation where name <> 'OpenCHS'; DELETE FROM audit where 1 = 1; ALTER SEQUENCE non_applicable_form_element_id_seq RESTART WITH 1; diff --git a/avni-server-api/src/test/resources/test-data-openchs-organisation.sql b/avni-server-api/src/test/resources/test-data-openchs-organisation.sql index 07975de20..b32d004b3 100644 --- a/avni-server-api/src/test/resources/test-data-openchs-organisation.sql +++ b/avni-server-api/src/test/resources/test-data-openchs-organisation.sql @@ -38,8 +38,7 @@ DELETE FROM address_level; DELETE FROM catchment; -DELETE -FROM account_admin; +DELETE FROM account_admin where admin_id <> (select id from users where username = 'admin'); delete from user_group; DELETE @@ -52,16 +51,14 @@ DELETE from message_receiver; DELETE from message_rule; -DELETE -FROM users; +DELETE FROM users where username <> 'admin'; DELETE FROM subject_type; DELETE FROM groups; DELETE FROM group_privilege; -DELETE -FROM organisation; +DELETE FROM organisation where name <> 'OpenCHS'; ALTER SEQUENCE form_element_id_seq RESTART WITH 1; ALTER SEQUENCE form_element_group_id_seq RESTART WITH 1; @@ -85,9 +82,3 @@ ALTER SEQUENCE individual_relation_id_seq RESTART WITH 1; ALTER SEQUENCE individual_relation_gender_mapping_id_seq RESTART WITH 1; ALTER SEQUENCE individual_relationship_type_id_seq RESTART WITH 1; ALTER SEQUENCE individual_relationship_id_seq RESTART WITH 1; - -INSERT INTO organisation (id, name, db_user, media_directory, uuid, schema_name, category_id, status_id) -VALUES (1, 'OpenCHS', 'openchs', 'openchs_impl', '3539a906-dfae-4ec3-8fbb-1b08f35c3884', 'openchs', 1, 1); - -INSERT INTO users (id, username, uuid, organisation_id, operating_individual_scope, name) -VALUES (1, 'admin', '5fed2907-df3a-4867-aef5-c87f4c78a31a', 1, 'None', 'admin'); diff --git a/avni-server-api/src/test/resources/test-data.sql b/avni-server-api/src/test/resources/test-data.sql index f83447a44..9ac4f3f38 100644 --- a/avni-server-api/src/test/resources/test-data.sql +++ b/avni-server-api/src/test/resources/test-data.sql @@ -22,9 +22,10 @@ DELETE FROM individual_relation_gender_mapping; DELETE FROM individual_relation; DELETE FROM gender; DELETE FROM catchment_address_mapping; +DELETE FROM location_location_mapping; DELETE FROM address_level; DELETE FROM catchment; -DELETE FROM account_admin; +DELETE FROM account_admin where admin_id <> (select id from users where username = 'admin'); DELETE FROM user_group; DELETE FROM external_system_config; DELETE FROM organisation_config; @@ -32,11 +33,12 @@ DELETE from message_request_queue; DELETE from message_receiver; DELETE from message_rule; DELETE FROM reset_sync; -DELETE FROM users; +DELETE FROM users where username <> 'admin'; DELETE FROM subject_type; DELETE FROM group_privilege; +DELETE FROM group_dashboard; DELETE FROM groups; -DELETE FROM organisation; +DELETE FROM organisation where name <> 'OpenCHS'; DELETE FROM audit; ALTER SEQUENCE non_applicable_form_element_id_seq RESTART WITH 1; @@ -68,10 +70,6 @@ ALTER SEQUENCE message_receiver_id_seq RESTART WITH 1; ALTER SEQUENCE message_request_queue_id_seq RESTART WITH 1; ALTER SEQUENCE message_rule_id_seq RESTART WITH 1; -INSERT into organisation(id, name, db_user, uuid, media_directory, parent_organisation_id, schema_name, category_id, status_id) -values (1, 'OpenCHS', 'openchs', '3539a906-dfae-4ec3-8fbb-1b08f35c3884', 'openchs_impl', null, 'openchs', 1, 1) -ON CONFLICT (uuid) DO NOTHING; - select create_db_user('demo', 'password'); INSERT INTO organisation(id, name, db_user, media_directory, uuid, parent_organisation_id, schema_name, category_id, status_id) @@ -90,8 +88,6 @@ ON CONFLICT (uuid) DO NOTHING; insert into subject_type(id, uuid, name, organisation_id, created_by_id, last_modified_by_id, created_date_time, last_modified_date_time) VALUES (1, '9f2af1f9-e150-4f8e-aad3-40bb7eb05aa3', 'Individual', 1, 1, 1, now(), now()); -INSERT INTO users (id, username, uuid, organisation_id, operating_individual_scope, is_org_admin, name) -VALUES (1, 'admin', '5fed2907-df3a-4867-aef5-c87f4c78a31a', 1, 'None', false, 'admin'); INSERT INTO users (id, username, uuid, organisation_id, operating_individual_scope, is_org_admin, name) VALUES (2, 'demo-admin', '0e53a72c-a109-49f2-918c-9599b266a585', 2, 'None', true, 'demo-admin'); INSERT INTO users (id, username, uuid, organisation_id, operating_individual_scope, is_org_admin, name) From 025659357ad88dfc44947e4e7deaa5dbd46e756f Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 28 Aug 2024 14:47:25 +0530 Subject: [PATCH 122/129] avniproject/avni-webapp#1306 - Location create csv check for no location provided. UserCatchment CSV - minor changes to error message. Fixed check for missing headers. Only add one error for a field. check wrong bool values in track location and beneficiary mode. Check for empty string instead of null value. Do not send regex in error message instead send in valid chars. General/All - ignore additional cells in data row. more test scenarios. --- .../org/avni/server/domain/JsonObject.java | 4 +- .../java/org/avni/server/domain/User.java | 4 +- .../batch/csv/writer/BulkLocationCreator.java | 28 ++-- .../csv/writer/UserAndCatchmentWriter.java | 67 ++++---- .../avni/server/importer/batch/model/Row.java | 14 +- .../org/avni/server/util/ValidationUtil.java | 4 +- .../java/org/avni/server/domain/UserTest.java | 1 - .../batch/csv/writer/BaseCSVImportTest.java | 7 +- .../BulkLocationCreatorIntegrationTest.java | 25 +++ .../BulkLocationEditorIntegrationTest.java | 85 +++++++--- ...UserAndCatchmentWriterIntegrationTest.java | 154 +++++++++++++++++- .../server/importer/batch/model/RowTest.java | 8 +- .../avni/server/util/ValidationUtilTest.java | 13 ++ 13 files changed, 329 insertions(+), 85 deletions(-) create mode 100644 avni-server-api/src/test/java/org/avni/server/util/ValidationUtilTest.java diff --git a/avni-server-api/src/main/java/org/avni/server/domain/JsonObject.java b/avni-server-api/src/main/java/org/avni/server/domain/JsonObject.java index 08df8bdf1..0f8365601 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/JsonObject.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/JsonObject.java @@ -21,8 +21,8 @@ public JsonObject with(String key, Object value) { return this; } - public JsonObject withEmptyCheckAndTrim(String key, String value){ - if(!S.isEmpty(value)){ + public JsonObject withEmptyCheckAndTrim(String key, String value) { + if (!S.isEmpty(value)) { super.put(key, value.trim()); } return this; diff --git a/avni-server-api/src/main/java/org/avni/server/domain/User.java b/avni-server-api/src/main/java/org/avni/server/domain/User.java index 1ab1d93b9..65363baa7 100644 --- a/avni-server-api/src/main/java/org/avni/server/domain/User.java +++ b/avni-server-api/src/main/java/org/avni/server/domain/User.java @@ -394,7 +394,7 @@ public static void validateUsername(String username, String userSuffix) { throw new ValidationException(String.format("Invalid username '%s'. Include correct userSuffix %s at the end", username, userSuffix)); } if (ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(username.trim(), ValidationUtil.COMMON_INVALID_CHARS_PATTERN)) { - throw new ValidationException(String.format("Invalid username '%s', contains atleast one disallowed character %s", username, ValidationUtil.COMMON_INVALID_CHARS_PATTERN)); + throw new ValidationException(String.format("Invalid username '%s', contains at least one disallowed character %s", username, ValidationUtil.COMMON_INVALID_CHARS)); } } @@ -403,7 +403,7 @@ public static void validateUsername(String username, String userSuffix) { */ public static void validateName(String name) { if (ValidationUtil.checkNullOrEmptyOrContainsDisallowedCharacters(name, ValidationUtil.NAME_INVALID_CHARS_PATTERN)) { - throw new ValidationException(String.format("Invalid name '%s', contains atleast one disallowed character %s", name, ValidationUtil.NAME_INVALID_CHARS_PATTERN)); + throw new ValidationException(String.format("Invalid name '%s', contains at least one disallowed character %s", name, ValidationUtil.NAME_INVALID_CHARS)); } } } diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java index 3c3b75277..e505e241f 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java @@ -1,5 +1,6 @@ package org.avni.server.importer.batch.csv.writer; +import com.google.common.collect.Sets; import org.avni.server.application.FormElement; import org.avni.server.builder.BuilderException; import org.avni.server.dao.AddressLevelTypeRepository; @@ -20,10 +21,7 @@ import org.springframework.util.StringUtils; import javax.transaction.Transactional; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; // This class is need so that the logic can be instantiated in integration tests. Spring batch configuration is not working in integration tests. @@ -68,23 +66,29 @@ public void createLocation(Row row, List allErrorMsgs, List loca } } - private List validateCreateModeHeaders(String[] headers, List allErrorMsgs, String locationHierarchy) { + private List validateHeaders(String[] headers, List allErrorMsgs, String locationHierarchy) { List headerList = Arrays.asList(headers); - List locationTypeHeaders = checkIfHeaderHasLocationTypesInOrderForHierarchy(locationHierarchy, headerList, allErrorMsgs); - List additionalHeaders = new ArrayList<>(headerList.subList(locationTypeHeaders.size(), headerList.size())); + List locationTypeHeaders = checkIfHeaderHasLocationTypesAndInOrderForHierarchy(locationHierarchy, headerList, allErrorMsgs); + List additionalHeaders = headerList.size() > locationTypeHeaders.size() ? new ArrayList<>(headerList.subList(locationTypeHeaders.size(), headerList.size())) : new ArrayList<>(); checkIfHeaderRowHasUnknownHeaders(additionalHeaders, allErrorMsgs); return locationTypeHeaders; } - private List checkIfHeaderHasLocationTypesInOrderForHierarchy(String locationHierarchy, List headerList, List allErrorMsgs) { - List locationTypeNamesForHierachy = importService.getAddressLevelTypesForCreateModeSingleHierarchy(locationHierarchy) + private List checkIfHeaderHasLocationTypesAndInOrderForHierarchy(String locationHierarchy, List headerList, List allErrorMsgs) { + List locationTypeNamesForHierarchy = importService.getAddressLevelTypesForCreateModeSingleHierarchy(locationHierarchy) .stream().map(AddressLevelType::getName).collect(Collectors.toList()); - if (headerList.size() >= locationTypeNamesForHierachy.size() && !headerList.subList(0, locationTypeNamesForHierachy.size()).equals(locationTypeNamesForHierachy)) { + HashSet expectedHeaders = new HashSet<>(locationTypeNamesForHierarchy); + if (Sets.difference(new HashSet<>(expectedHeaders), new HashSet<>(headerList)).size() == locationTypeNamesForHierarchy.size()) { allErrorMsgs.add(LocationTypesHeaderError); throw new RuntimeException(String.join(", ", allErrorMsgs)); } - return locationTypeNamesForHierachy; + + if (headerList.size() >= locationTypeNamesForHierarchy.size() && !headerList.subList(0, locationTypeNamesForHierarchy.size()).equals(locationTypeNamesForHierarchy)) { + allErrorMsgs.add(LocationTypesHeaderError); + throw new RuntimeException(String.join(", ", allErrorMsgs)); + } + return locationTypeNamesForHierarchy; } private void checkIfHeaderRowHasUnknownHeaders(List additionalHeaders, List allErrorMsgs) { @@ -136,7 +140,7 @@ private void validateRow(Row row, List hierarchicalLocationTypeNames, Li @Transactional(Transactional.TxType.REQUIRES_NEW) public void write(List rows, String idBasedLocationHierarchy) { List allErrorMsgs = new ArrayList<>(); - List hierarchicalLocationTypeNames = validateCreateModeHeaders(rows.get(0).getHeaders(), allErrorMsgs, idBasedLocationHierarchy); + List hierarchicalLocationTypeNames = validateHeaders(rows.get(0).getHeaders(), allErrorMsgs, idBasedLocationHierarchy); for (Row row : rows) { if (skipRow(row, hierarchicalLocationTypeNames)) { continue; diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java index 74be9a2f9..8f5f97609 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriter.java @@ -1,6 +1,5 @@ package org.avni.server.importer.batch.csv.writer; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import org.avni.server.dao.LocationRepository; import org.avni.server.dao.UserRepository; @@ -10,6 +9,7 @@ import org.avni.server.importer.batch.csv.writer.header.UsersAndCatchmentsHeaders; import org.avni.server.importer.batch.model.Row; import org.avni.server.service.*; +import org.avni.server.util.PhoneNumberUtil; import org.avni.server.util.RegionUtil; import org.avni.server.util.S; import org.avni.server.web.request.syncAttribute.UserSyncSettings; @@ -32,7 +32,6 @@ @Component public class UserAndCatchmentWriter implements ItemWriter, Serializable { - public static final String ERR_MSG_DELIMITER = "\",\""; private final UserService userService; private final CatchmentService catchmentService; private final LocationRepository locationRepository; @@ -44,14 +43,15 @@ public class UserAndCatchmentWriter implements ItemWriter, Serializable { private final Pattern compoundHeaderPattern; private final ResetSyncService resetSyncService; - private static final String ERR_MSG_MANDATORY_FIELD = "Invalid or Empty value specified for mandatory field %s"; - private static final String ERR_MSG_LOCATION_FIELD = "Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'"; - private static final String ERR_MSG_LOCALE_FIELD = "Provided value '%s' for Preferred Language is invalid"; - private static final String ERR_MSG_DATE_PICKER_FIELD = "Provided value '%s' for Date picker mode is invalid"; - private static final String ERR_MSG_UNKNOWN_HEADERS = "Unknown headers - %s included in file. Please refer to sample file for valid list of headers"; + private static final String ERR_MSG_MANDATORY_OR_INVALID_FIELD = "Invalid or Empty value specified for mandatory field %s."; + private static final String ERR_MSG_LOCATION_FIELD = "Provided Location does not exist in Avni. Please add it or check for spelling mistakes '%s'."; + private static final String ERR_MSG_LOCALE_FIELD = "Provided value '%s' for Preferred Language is invalid."; + private static final String ERR_MSG_DATE_PICKER_FIELD = "Provided value '%s' for Date picker mode is invalid."; + private static final String ERR_MSG_INVALID_PHONE_NUMBER = "Provided value '%s' for phone number is invalid."; + private static final String ERR_MSG_UNKNOWN_HEADERS = "Unknown headers - %s included in file. Please refer to sample file for valid list of headers."; private static final String ERR_MSG_MISSING_MANDATORY_FIELDS = "Mandatory columns are missing from uploaded file - %s. Please refer to sample file for the list of mandatory headers."; - private static final String ERR_MSG_INVALID_CONCEPT_ANSWER = "'%s' is not a valid value for the concept '%s'" + - "To input this value, add this as an answer to the coded concept '%s'"; + private static final String ERR_MSG_INVALID_CONCEPT_ANSWER = "'%s' is not a valid value for the concept '%s'. " + + "To input this value, add this as an answer to the coded concept '%s'."; private static final String METADATA_ROW_START_STRING = "Mandatory field."; private static final List DATE_PICKER_MODE_OPTIONS = Arrays.asList("calendar", "spinner"); @@ -93,7 +93,7 @@ private void validateHeaders(String[] headers) { checkForMissingHeaders(headerList, allErrorMsgs, expectedStandardHeaders, syncAttributeHeadersForSubjectTypes); checkForUnknownHeaders(headerList, allErrorMsgs, expectedStandardHeaders, syncAttributeHeadersForSubjectTypes); if (!allErrorMsgs.isEmpty()) { - throw new RuntimeException(String.join(ERR_MSG_DELIMITER, allErrorMsgs)); + throw new RuntimeException(createMultiErrorMessage(allErrorMsgs)); } } @@ -107,11 +107,10 @@ private void checkForUnknownHeaders(List headerList, List allErr } } - private void checkForMissingHeaders(List headerList, List allErrorMsgs, List expectedStandardHeaders, List syncAttributeHeadersForSubjectTypes) { + private void checkForMissingHeaders(List headerList, List allErrorMsgs, List expectedStandardHeaders, List expectedSyncAttributeHeadersForSubjectTypes) { HashSet expectedHeaders = new HashSet<>(expectedStandardHeaders); - expectedHeaders.addAll(syncAttributeHeadersForSubjectTypes); + expectedHeaders.addAll(expectedSyncAttributeHeadersForSubjectTypes); HashSet presentHeaders = new HashSet<>(headerList); - presentHeaders.addAll(syncAttributeHeadersForSubjectTypes); Sets.SetView missingHeaders = Sets.difference(expectedHeaders, presentHeaders); if (!missingHeaders.isEmpty()) { allErrorMsgs.add(String.format(ERR_MSG_MISSING_MANDATORY_FIELDS, String.join(", ", missingHeaders))); @@ -138,7 +137,7 @@ private void write(Row row) throws IDPException { Organisation organisation = UserContextHolder.getUserContext().getOrganisation(); String userSuffix = "@".concat(organisation.getEffectiveUsernameSuffix()); JsonObject syncSettings = constructSyncSettings(row, rowValidationErrorMsgs); - validateRowAndAssimilateErrors(rowValidationErrorMsgs, fullAddress, catchmentName, nameOfUser, username, email, phoneNumber, language, datePickerMode, location, locale, userSuffix); + validateRowAndAssimilateErrors(rowValidationErrorMsgs, fullAddress, catchmentName, nameOfUser, username, email, phoneNumber, language, datePickerMode, location, locale, userSuffix, trackLocation, beneficiaryMode); Catchment catchment = catchmentService.createOrUpdate(catchmentName, location); User user = userRepository.findByUsername(username.trim()); User currentUser = userService.getCurrentUser(); @@ -175,26 +174,34 @@ private void write(Row row) throws IDPException { } } - private void validateRowAndAssimilateErrors(List rowValidationErrorMsgs, String fullAddress, String catchmentName, String nameOfUser, String username, String email, String phoneNumber, String language, String datePickerMode, AddressLevel location, Locale locale, String userSuffix) { - addErrMsgIfValidationFails(!StringUtils.hasLength(catchmentName), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, CATCHMENT_NAME)); - addErrMsgIfValidationFails(!StringUtils.hasLength(username), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, USERNAME)); - addErrMsgIfValidationFails(!StringUtils.hasLength(nameOfUser), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, FULL_NAME_OF_USER)); - addErrMsgIfValidationFails(!StringUtils.hasLength(email), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, EMAIL_ADDRESS)); - addErrMsgIfValidationFails(!StringUtils.hasLength(phoneNumber), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_FIELD, MOBILE_NUMBER)); + private void validateRowAndAssimilateErrors(List rowValidationErrorMsgs, String fullAddress, String catchmentName, String nameOfUser, String username, String email, String phoneNumber, String language, String datePickerMode, AddressLevel location, Locale locale, String userSuffix, Boolean trackLocation, Boolean beneficiaryMode) { + addErrMsgIfValidationFails(!StringUtils.hasLength(catchmentName), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_OR_INVALID_FIELD, CATCHMENT_NAME)); + if (!addErrMsgIfValidationFails(!StringUtils.hasLength(username), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_OR_INVALID_FIELD, USERNAME))) + extractUserUsernameValidationErrMsg(rowValidationErrorMsgs, username, userSuffix); + if (!addErrMsgIfValidationFails(!StringUtils.hasLength(nameOfUser), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_OR_INVALID_FIELD, FULL_NAME_OF_USER))) + extractUserNameValidationErrMsg(rowValidationErrorMsgs, nameOfUser); + if (!addErrMsgIfValidationFails(!StringUtils.hasLength(email), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_OR_INVALID_FIELD, EMAIL_ADDRESS))) + extractUserEmailValidationErrMsg(rowValidationErrorMsgs, email); + if (!addErrMsgIfValidationFails(!StringUtils.hasLength(phoneNumber), rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_OR_INVALID_FIELD, MOBILE_NUMBER))) + addErrMsgIfValidationFails(!PhoneNumberUtil.isValidPhoneNumber(phoneNumber, RegionUtil.getCurrentUserRegion()), rowValidationErrorMsgs, format(ERR_MSG_INVALID_PHONE_NUMBER, MOBILE_NUMBER)); - addErrMsgIfValidationFails(Objects.isNull(location), rowValidationErrorMsgs, format(ERR_MSG_LOCATION_FIELD, fullAddress)); - addErrMsgIfValidationFails(Objects.isNull(locale), rowValidationErrorMsgs, format(ERR_MSG_LOCALE_FIELD, language)); - addErrMsgIfValidationFails(Objects.isNull(datePickerMode) || !DATE_PICKER_MODE_OPTIONS.contains(datePickerMode), + addErrMsgIfValidationFails(StringUtils.isEmpty(location), rowValidationErrorMsgs, format(ERR_MSG_LOCATION_FIELD, fullAddress)); + addErrMsgIfValidationFails(StringUtils.isEmpty(locale), rowValidationErrorMsgs, format(ERR_MSG_LOCALE_FIELD, language)); + addErrMsgIfValidationFails(StringUtils.isEmpty(datePickerMode) || !DATE_PICKER_MODE_OPTIONS.contains(datePickerMode), rowValidationErrorMsgs, format(ERR_MSG_DATE_PICKER_FIELD, datePickerMode)); - extractUserUsernameValidationErrMsg(rowValidationErrorMsgs, username, userSuffix); - extractUserNameValidationErrMsg(rowValidationErrorMsgs, nameOfUser); - extractUserEmailValidationErrMsg(rowValidationErrorMsgs, email); + addErrMsgIfValidationFails(trackLocation == null, rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_OR_INVALID_FIELD, TRACK_LOCATION)); + addErrMsgIfValidationFails(beneficiaryMode == null, rowValidationErrorMsgs, format(ERR_MSG_MANDATORY_OR_INVALID_FIELD, ENABLE_BENEFICIARY_MODE)); + if (!rowValidationErrorMsgs.isEmpty()) { - throw new RuntimeException(String.join(ERR_MSG_DELIMITER, rowValidationErrorMsgs)); + throw new RuntimeException(createMultiErrorMessage(rowValidationErrorMsgs)); } } + private static String createMultiErrorMessage(List errorMsgs) { + return errorMsgs.stream().map(s -> "\"" + s + "\"").collect(Collectors.joining("; ")); + } + private void extractUserNameValidationErrMsg(List rowValidationErrorMsgs, String nameOfUser) { try { User.validateName(nameOfUser); @@ -219,10 +226,11 @@ private void extractUserUsernameValidationErrMsg(List rowValidationError } } - private void addErrMsgIfValidationFails(boolean validationCheckResult, List rowValidationErrorMsgs, String validationErrorMessage) { + private boolean addErrMsgIfValidationFails(boolean validationCheckResult, List rowValidationErrorMsgs, String validationErrorMessage) { if (validationCheckResult) { rowValidationErrorMsgs.add(validationErrorMessage); } + return validationCheckResult; } private JsonObject constructSyncSettings(Row row, List rowValidationErrorMsgs) { @@ -245,7 +253,8 @@ private void updateSyncSettingsFor(String saHeader, Row row, Map { public static final Pattern TRUE_VALUE = Pattern.compile("y|yes|true|1", Pattern.CASE_INSENSITIVE); + public static final Pattern FALSE_VALUE = Pattern.compile("n|no|false|0", Pattern.CASE_INSENSITIVE); private final String[] headers; private final String[] values; public Row(String[] headers, String[] values) { this.headers = headers; this.values = values; - IntStream.range(0, values.length).forEach(index -> this.put(headers[index].trim(), values[index].trim())); + IntStream.range(0, headers.length).forEach(index -> { + this.put(headers[index].trim(), values.length > index ? values[index].trim() : ""); + }); } private String nullSafeTrim(String s) { @@ -52,12 +55,17 @@ public String getOrDefault(Object key, String defaultValue) { @Override public String toString() { return IntStream.range(0, headers.length) - .mapToObj(index -> index < values.length? format("\"%s\"", values[index]): "\"\"") + .mapToObj(index -> index < values.length ? format("\"%s\"", values[index]) : "\"\"") .reduce((c1, c2) -> format("%s,%s", c1, c2)) .get(); } public Boolean getBool(String header) { - return TRUE_VALUE.matcher(String.valueOf(get(header))).matches(); + if (TRUE_VALUE.matcher(String.valueOf(get(header))).matches()) { + return true; + } else if (FALSE_VALUE.matcher(String.valueOf(get(header))).matches()) { + return false; + } + return null; } } diff --git a/avni-server-api/src/main/java/org/avni/server/util/ValidationUtil.java b/avni-server-api/src/main/java/org/avni/server/util/ValidationUtil.java index f22abe941..509f4ef3e 100644 --- a/avni-server-api/src/main/java/org/avni/server/util/ValidationUtil.java +++ b/avni-server-api/src/main/java/org/avni/server/util/ValidationUtil.java @@ -5,14 +5,16 @@ public class ValidationUtil { public static final Pattern COMMON_INVALID_CHARS_PATTERN = Pattern.compile("^.*[<>=\"'].*$"); + public static final Pattern COMMON_INVALID_CHARS = Pattern.compile("<> = \" '"); public static final Pattern NAME_INVALID_CHARS_PATTERN = Pattern.compile("^.*[<>=\"].*$"); + public static final Pattern NAME_INVALID_CHARS = Pattern.compile("<> = \""); public static boolean checkNull(Object checkObject) { return checkObject == null; } public static boolean checkEmptyString(String checkString) { - return checkString.trim().equals(""); + return checkString.trim().isEmpty(); } public static boolean containsDisallowedPattern(String checkString, Pattern pattern) { diff --git a/avni-server-api/src/test/java/org/avni/server/domain/UserTest.java b/avni-server-api/src/test/java/org/avni/server/domain/UserTest.java index dc1806157..f531c00f6 100644 --- a/avni-server-api/src/test/java/org/avni/server/domain/UserTest.java +++ b/avni-server-api/src/test/java/org/avni/server/domain/UserTest.java @@ -6,7 +6,6 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; public class UserTest { @Test diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java index e9ef5976b..3a296ddd0 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BaseCSVImportTest.java @@ -1,9 +1,6 @@ package org.avni.server.importer.batch.csv.writer; import org.avni.server.common.AbstractControllerIntegrationTest; -import org.avni.server.domain.AddressLevel; - -import static org.junit.Assert.assertNotNull; public abstract class BaseCSVImportTest extends AbstractControllerIntegrationTest { protected String[] header(String... cells) { @@ -21,4 +18,8 @@ protected String[] lineage(String ... lineage) { protected String error(String message) { return message; } + + protected String hasError(String s) { + return s; + } } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java index 8dd8ce855..583bbe568 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreatorIntegrationTest.java @@ -121,6 +121,26 @@ private void failure(String[] headers, String[] cells, String errorMessage) { assertEquals(before, after); } + private void failsOnMissingHeader(String[] headers, String... errorMessages) { + try { + bulkLocationCreator.write(Collections.singletonList(new Row(headers, new String[0])), hierarchy); + fail(); + } catch (RuntimeException e) { + String message = e.getMessage(); + if (message == null) { + e.printStackTrace(); + fail(); + } else { + Arrays.stream(errorMessages).forEach(s -> { + if (!message.contains(s)) { + e.printStackTrace(); + fail("Expected error message: " + s + " not present in: " + message); + } + }); + } + } + } + private String[] lineageExists(String... lineage) { return lineage; } @@ -241,6 +261,11 @@ public void shouldCreate() { dataRow("Bihar", "Vaishali", " Mahua ", "23.45,43.85"), newLocationsCreated(0)); + // missing headers + failsOnMissingHeader( + header(), + hasError("Location types missing or not in order in header for specified Location Hierarchy. Please refer to sample file for valid list of headers.") + ); treatAsDescriptor(header("State", "District", "Block", "GPS coordinates"), dataRow("Example: state 1", "Ex: distr 1", "Ex: blo 1", "Ex. 23.45,43.85")); diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java index ad9dc0c90..b7d0b82ae 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/BulkLocationEditorIntegrationTest.java @@ -104,6 +104,54 @@ private String[] verifyNotExists(String... strings) { return strings; } + private void treatAsDescriptor(String[] headers, String... descriptorCells) { + long before = locationRepository.count(); + bulkLocationEditor.write(Collections.singletonList(new Row(headers, descriptorCells))); + long after = locationRepository.count(); + assertEquals(before, after); + } + + private void success(String[] headers, String[] dataRow, String[] exists, String[] ... notExists) { + bulkLocationEditor.editLocation(new Row(headers, dataRow), new ArrayList<>()); + lineageExists(exists); + for (String[] notExist : notExists) { + lineageNotExists(notExist); + } + } + + private void failure(String[] headers, String[] dataRow, String errorMessage, String[] exists, String[] ... notExists) { + try { + bulkLocationEditor.editLocation(new Row(headers, dataRow), new ArrayList<>()); + fail(); + } catch (Exception exception) { + assertEquals(errorMessage, exception.getMessage()); + } + lineageExists(exists); + for (String[] notExist : notExists) { + lineageNotExists(notExist); + } + } + + private void failsOnMissingHeader(String[] headers, String... errorMessages) { + try { + bulkLocationEditor.write(Collections.singletonList(new Row(headers, new String[0]))); + fail(); + } catch (RuntimeException e) { + String message = e.getMessage(); + if (message == null) { + e.printStackTrace(); + fail(); + } else { + Arrays.stream(errorMessages).forEach(s -> { + if (!message.contains(s)) { + e.printStackTrace(); + fail("Expected error message: " + s + " not present in: " + message); + } + }); + } + } + } + @Test public void shouldEdit() { // no change @@ -153,33 +201,16 @@ public void shouldEdit() { "Enter new name here ONLY if it needs to be updated", "Hierarchy of parent location that should contain the child location", "Ex: 23.45,43.85")); - } - - private void treatAsDescriptor(String[] headers, String... descriptorCells) { - long before = locationRepository.count(); - bulkLocationEditor.write(Collections.singletonList(new Row(headers, descriptorCells))); - long after = locationRepository.count(); - assertEquals(before, after); - } - - private void success(String[] headers, String[] dataRow, String[] exists, String[] ... notExists) { - bulkLocationEditor.editLocation(new Row(headers, dataRow), new ArrayList<>()); - lineageExists(exists); - for (String[] notExist : notExists) { - lineageNotExists(notExist); - } - } - private void failure(String[] headers, String[] dataRow, String errorMessage, String[] exists, String[] ... notExists) { - try { - bulkLocationEditor.editLocation(new Row(headers, dataRow), new ArrayList<>()); - fail(); - } catch (Exception exception) { - assertEquals(errorMessage, exception.getMessage()); - } - lineageExists(exists); - for (String[] notExist : notExists) { - lineageNotExists(notExist); - } + // missing header - nothing provided + failsOnMissingHeader( + header(), + hasError("'Location with full hierarchy' is required, At least one of 'New location name', 'GPS coordinates' or 'Parent location with full hierarchy' is required") + ); + // missing header - New location name + failsOnMissingHeader( + header("Location with full hierarchy"), + hasError("At least one of 'New location name', 'GPS coordinates' or 'Parent location with full hierarchy'") + ); } } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriterIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriterIntegrationTest.java index 7986a3d55..9b248861e 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriterIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/csv/writer/UserAndCatchmentWriterIntegrationTest.java @@ -1,14 +1,14 @@ package org.avni.server.importer.batch.csv.writer; -import org.avni.server.application.*; import org.avni.server.dao.CatchmentRepository; -import org.avni.server.dao.LocationRepository; import org.avni.server.dao.UserRepository; import org.avni.server.dao.application.FormRepository; -import org.avni.server.domain.*; +import org.avni.server.domain.AddressLevel; +import org.avni.server.domain.AddressLevelType; +import org.avni.server.domain.Concept; +import org.avni.server.domain.ConceptDataType; import org.avni.server.domain.factory.AddressLevelBuilder; import org.avni.server.domain.factory.AddressLevelTypeBuilder; -import org.avni.server.domain.factory.metadata.TestFormBuilder; import org.avni.server.domain.metadata.SubjectTypeBuilder; import org.avni.server.importer.batch.model.Row; import org.avni.server.service.IDPException; @@ -25,6 +25,7 @@ import java.util.UUID; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(value = {"/tear-down.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @@ -98,6 +99,50 @@ private void success(String[] headers, String[] cells, boolean catchmentCreated, assertEquals(userRepository.count(), numberOfUsers); } + private void failure(String[] headers, String[] cells, String... errorMessages) throws IDPException { + try { + userAndCatchmentWriter.write(Collections.singletonList(new Row(headers, cells))); + fail(); + } catch (RuntimeException e) { + String message = e.getMessage(); + if (message == null) { + e.printStackTrace(); + fail(); + } else { + Arrays.stream(errorMessages).forEach(s -> { + if (!message.contains(s)) { + e.printStackTrace(); + fail("Expected error message: " + s + " not present in: " + message); + } + }); + } + } + } + + private void failsOnMissingHeader(String[] headers, String... errorMessages) throws IDPException { + try { + userAndCatchmentWriter.write(Collections.singletonList(new Row(headers, new String[]{}))); + fail(); + } catch (RuntimeException e) { + String message = e.getMessage(); + if (message == null) { + e.printStackTrace(); + fail(); + } else { + Arrays.stream(errorMessages).forEach(s -> { + if (!message.contains(s)) { + e.printStackTrace(); + fail("Expected error message: " + s + " not present in: " + message); + } + }); + } + } + } + + private void treatAsDescriptor(String[] headers, String... additionalHeaders) throws IDPException { + success(headers, additionalHeaders, false, false); + } + @Test public void shouldCreateUpdate() throws IDPException { // new catchment, new user @@ -106,5 +151,106 @@ public void shouldCreateUpdate() throws IDPException { dataRow("Bihar, District1, Block11", "Catchment 1", "username1@example", "User 1", "username1@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1", "Answer 1"), catchmentCreated(true), userCreated(true)); + // existing catchment new user + success( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", "Catchment 1", "username2@example", "User 2", "username2@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1", "Answer 1"), + catchmentCreated(false), + userCreated(true)); + // new catchment existing user + success( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", "Catchment 3", "username2@example", "User 2", "username2@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1", "Answer 1"), + catchmentCreated(true), + userCreated(false)); + // existing catchment existing user + success( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", "Catchment 3", "username2@example", "User 2", "username2@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1", "Answer 1"), + catchmentCreated(false), + userCreated(false)); + // with spaces + success( + header(" Location with full hierarchy", " Catchment Name", "Username ", " Full Name of User", "Email Address", "Mobile Number", " Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", " Identifier Prefix", " User Groups", " SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow(" Bihar, District1, Block11", " Catchment 4", " username3@example", " User 3", " username3@example.com ", " 9455509147 ", "English ", "true ", " spinner", " false", "", " User Group 1", " Answer 1"), + catchmentCreated(true), + userCreated(true)); + + // wrong - username, email, phone number, language, track location, date picker mode, enable beneficiary mode + failure( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", "Catchment 1", "username1@exmplee", "User 1", "username1@examplecom", "9455047", "Irish", "truee", "spinnerr", "falsee", "", "User Group 1", "Answer 1"), + hasError("Invalid username 'username1@exmplee'. Include correct userSuffix @example at the end"), + hasError("Invalid email address username1@examplecom"), + hasError("Provided value 'Mobile Number' for phone number is invalid."), + hasError("Provided value 'Irish' for Preferred Language is invalid."), + hasError("Provided value 'spinnerr' for Date picker mode is invalid."), + hasError("Invalid or Empty value specified for mandatory field Track Location"), + hasError("Invalid or Empty value specified for mandatory field Enable Beneficiary") + ); + // empty - catchment name, username, Full Name of User, email, phone number, track location, date picker mode, enable beneficiary mode + failure( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", " ", "", " ", " ", " ", " ", " ", "", "", "", "User Group 1", "Answer 1"), + hasError("Invalid or Empty value specified for mandatory field Catchment Name."), + hasError("Invalid or Empty value specified for mandatory field Username."), + hasError("Invalid or Empty value specified for mandatory field Full Name of User."), + hasError("Invalid or Empty value specified for mandatory field Email Address."), + hasError("Invalid or Empty value specified for mandatory field Mobile Number."), + hasError("Provided value '' for Date picker mode is invalid."), + hasError("Invalid or Empty value specified for mandatory field Track Location."), + hasError("Invalid or Empty value specified for mandatory field Enable Beneficiary") + ); + + // invalid User Group Name + failure( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", "Catchment 3", "username2@example", "User 2", "username2@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1345", "Answer 1"), + hasError("Group 'User Group 1345' not found") + ); + // same user group twice + success( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", "Catchment 3", "username4@example", "User 4", "username4@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1,User Group 1", "Answer 1"), + catchmentCreated(false), + userCreated(true) + ); + + // invalid sync attribute + failure( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", "Catchment 3", "username2@example", "User 2", "username2@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1", "Answer 1223"), + hasError("'Answer 1223' is not a valid value for the concept 'Sync Concept'.") + ); + + // Wrong location hierarchy + failure( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, NoBlock11", "Catchment 3", "username2@example", "User 2", "username2@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1", "Answer 1"), + hasError("Provided Location does not exist in Avni. Please add it or check for spelling mistakes 'Bihar, District1, NoBlock11'.") + ); + + // Missing headers - sync attributes + failsOnMissingHeader( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups"), + hasError("Mandatory columns are missing from uploaded file - SubjectTypeWithSyncAttributeBasedSync->Sync Concept. Please refer to sample file for the list of mandatory headers.") + ); + // Missing headers - all + failsOnMissingHeader( + header(), + hasError("Mandatory columns are missing from uploaded file - Track Location, Identifier Prefix, Catchment Name, Full Name of User, Mobile Number, Enable Beneficiary mode, User Groups, Username, SubjectTypeWithSyncAttributeBasedSync->Sync Concept, Preferred Language, Location with full hierarchy, Date picker mode, Email Address. Please refer to sample file for the list of mandatory headers.") + ); + + // allow additional cells in data row + success( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Bihar, District1, Block11", "Catchment 3", "username5@example", "User 5", "username5@example.com", "9455509147", "English", "true", "spinner", "false", "", "User Group 1", "Answer 1", "Foo"), + catchmentCreated(false), + userCreated(true) + ); + + treatAsDescriptor( + header("Location with full hierarchy", "Catchment Name", "Username", "Full Name of User", "Email Address", "Mobile Number", "Preferred Language", "Track Location", "Date picker mode", "Enable Beneficiary mode", "Identifier Prefix", "User Groups", "SubjectTypeWithSyncAttributeBasedSync->Sync Concept"), + dataRow("Mandatory field. Can be found from Admin -> Locations -> Click Export", "Mandatory field", "Mandatory field", "Mandatory field", "Mandatory field", "Mandatory field. Prefix the mobile number with country code", "Allowed values: {English, Hindi}. Only single value allowed. Default: English", "Allowed values: yes, no. Default: no", "Allowed values: calendar, spinner. Default: calendar", "Allowed values: yes, no. Default: no", "", "Allowed values: {Administrators, Everyone}")); } } diff --git a/avni-server-api/src/test/java/org/avni/server/importer/batch/model/RowTest.java b/avni-server-api/src/test/java/org/avni/server/importer/batch/model/RowTest.java index e8d5d0b92..eb569a6c1 100644 --- a/avni-server-api/src/test/java/org/avni/server/importer/batch/model/RowTest.java +++ b/avni-server-api/src/test/java/org/avni/server/importer/batch/model/RowTest.java @@ -6,13 +6,19 @@ public class RowTest { @Test - public void toStringShouldSerialiseProperly() throws Exception { + public void toStringShouldSerialiseProperly() { String[] headers = {"A", "B"}; assertEquals("\"AA\",\"\"", new Row(headers, new String[]{"AA"}).toString()); assertEquals("\"AA\",\"BB\"", new Row(headers, new String[]{"AA", "BB"}).toString()); assertEquals("\"AB, CD\",\"BB, EE\"", new Row(headers, new String[]{"AB, CD", "BB, EE"}).toString()); } + @Test + public void allowExtraColumnsInData() { + String[] headers = {"A", "B"}; + new Row(headers, new String[]{"AA", "BB", "CC"}); + } + @Test public void trimHeadersAndValues() { String[] headers = {"A", "B"}; diff --git a/avni-server-api/src/test/java/org/avni/server/util/ValidationUtilTest.java b/avni-server-api/src/test/java/org/avni/server/util/ValidationUtilTest.java new file mode 100644 index 000000000..9a41c1de4 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/util/ValidationUtilTest.java @@ -0,0 +1,13 @@ +package org.avni.server.util; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class ValidationUtilTest { + @Test + public void checkChars() { + assertTrue(ValidationUtil.containsDisallowedPattern(">", ValidationUtil.COMMON_INVALID_CHARS_PATTERN)); + assertTrue(ValidationUtil.containsDisallowedPattern("\"", ValidationUtil.COMMON_INVALID_CHARS_PATTERN)); + } +} From 6c5a334fe4b397aafaff4e52fb67820216eb5067 Mon Sep 17 00:00:00 2001 From: Joy A Date: Wed, 28 Aug 2024 18:46:28 +0530 Subject: [PATCH 123/129] avniproject/avni-client#1460 | Catchment not setup for user - error handling --- .../server/service/ScopeAwareService.java | 2 ++ .../org/avni/server/web/MediaController.java | 19 +++++++++++++---- .../org/avni/server/web/SyncController.java | 21 ++++++++++++------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/service/ScopeAwareService.java b/avni-server-api/src/main/java/org/avni/server/service/ScopeAwareService.java index 99a3201ad..787141187 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ScopeAwareService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ScopeAwareService.java @@ -7,6 +7,7 @@ import org.avni.server.domain.SubjectType; import org.avni.server.domain.User; import org.avni.server.framework.ApplicationContextProvider; +import org.avni.server.web.validation.ValidationException; import org.joda.time.DateTime; import java.util.List; @@ -20,6 +21,7 @@ default boolean isChangedBySubjectTypeRegistrationLocationType(User user, DateTi } default boolean isChangedByCatchment(User user, DateTime lastModifiedDateTime, SyncEntityName syncEntityName) { + if (user.getCatchment() == null) throw new ValidationException("NoCatchmentFound"); return repository().isEntityChanged(new SyncParameters(lastModifiedDateTime, DateTime.now(), null, null, null, null, null, user.getSyncSettings(), syncEntityName, user.getCatchment())); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/MediaController.java b/avni-server-api/src/main/java/org/avni/server/web/MediaController.java index 0bdc3e668..d1fca591f 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/MediaController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/MediaController.java @@ -27,7 +27,6 @@ import javax.servlet.http.HttpServletResponse; import java.awt.*; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URL; @@ -76,7 +75,11 @@ private ResponseEntity getFileUrlResponse(String fileName, HttpMethod me @PreAuthorize(value = "hasAnyAuthority('user')") public ResponseEntity generateMobileDatabaseBackupUploadUrl() { logger.info("getting mobile database backup upload url"); - return getFileUrlResponse(mobileDatabaseBackupFile(), HttpMethod.PUT); + try { + return getFileUrlResponse(mobileDatabaseBackupFile(), HttpMethod.PUT); + } catch (ValidationException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } } private String mobileDatabaseBackupFile() { @@ -92,14 +95,22 @@ private String mobileDatabaseBackupFile() { @PreAuthorize(value = "hasAnyAuthority('user')") public ResponseEntity generateMobileDatabaseBackupDownloadUrl() { logger.info("getting mobile database backup download url"); - return getFileUrlResponse(mobileDatabaseBackupFile(), HttpMethod.GET); + try { + return getFileUrlResponse(mobileDatabaseBackupFile(), HttpMethod.GET); + } catch (ValidationException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } } @RequestMapping(value = "/media/mobileDatabaseBackupUrl/exists", method = RequestMethod.GET) @PreAuthorize(value = "hasAnyAuthority('user')") public ResponseEntity mobileDatabaseBackupExists() { logger.info("checking whether mobile database backup url exists"); - return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(Boolean.toString(s3Service.fileExists(mobileDatabaseBackupFile()))); + try { + return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(Boolean.toString(s3Service.fileExists(mobileDatabaseBackupFile()))); + } catch (ValidationException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } } @RequestMapping(value = "/media/signedUrl", method = RequestMethod.GET) diff --git a/avni-server-api/src/main/java/org/avni/server/web/SyncController.java b/avni-server-api/src/main/java/org/avni/server/web/SyncController.java index 5a16fb1c6..52535bded 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/SyncController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/SyncController.java @@ -8,11 +8,13 @@ import org.avni.server.service.accessControl.PrivilegeService; import org.avni.server.service.application.MenuItemService; import org.avni.server.web.request.EntitySyncStatusContract; +import org.avni.server.web.validation.ValidationException; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; @@ -298,13 +300,18 @@ public ResponseEntity getSyncDetailsWithScopeAwareEAS(@RequestBody List allSyncableItems = syncDetailService.getAllSyncableItems(true, includeUserSubjectType); long afterSyncDetailsService = new DateTime().getMillis(); logger.info(String.format("Time taken for syncDetailsService %d", afterSyncDetailsService - now.getMillis())); - List changedEntities = getChangedEntities(entitySyncStatusContracts, allSyncableItems, true); - logger.info(String.format("Time taken for stuff %d", new DateTime().getMillis() - afterSyncDetailsService)); - return ResponseEntity.ok().body(new JsonObject() - .with("syncDetails", changedEntities) - .with("now", now) - .with("nowMinus10Seconds", nowMinus10Seconds) - ); + try { + List changedEntities = getChangedEntities(entitySyncStatusContracts, allSyncableItems, true); + logger.info(String.format("Time taken for stuff %d", new DateTime().getMillis() - afterSyncDetailsService)); + return ResponseEntity.ok().body(new JsonObject() + .with("syncDetails", changedEntities) + .with("now", now) + .with("nowMinus10Seconds", nowMinus10Seconds) + ); + } catch (ValidationException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } + } /** From 73d8c2d61cc834f114d2cce5d7c3a44f46f3953c Mon Sep 17 00:00:00 2001 From: himeshr Date: Wed, 28 Aug 2024 18:55:11 +0530 Subject: [PATCH 124/129] avniproject/avni-webapp#1254 | Remove FEG.display column and setters, use FEG.name instead --- .../org/avni/server/application/FormElementGroup.java | 8 +------- .../main/java/org/avni/server/builder/FormBuilder.java | 1 - .../org/avni/server/builder/FormElementGroupBuilder.java | 5 ----- .../src/main/java/org/avni/server/web/FormController.java | 1 - .../web/request/application/FormElementGroupContract.java | 7 +------ .../migration/V1_344__OverwriteFEGNameWithDisplayName.sql | 2 ++ .../server/application/TestFormElementGroupBuilder.java | 5 ----- 7 files changed, 4 insertions(+), 25 deletions(-) create mode 100644 avni-server-api/src/main/resources/db/migration/V1_344__OverwriteFEGNameWithDisplayName.sql diff --git a/avni-server-api/src/main/java/org/avni/server/application/FormElementGroup.java b/avni-server-api/src/main/java/org/avni/server/application/FormElementGroup.java index 3afb80929..6d0199e2b 100644 --- a/avni-server-api/src/main/java/org/avni/server/application/FormElementGroup.java +++ b/avni-server-api/src/main/java/org/avni/server/application/FormElementGroup.java @@ -19,9 +19,6 @@ public class FormElementGroup extends OrganisationAwareEntity { @NotNull private String name; - @Column - private String display; - @NotNull private Double displayOrder; @@ -99,12 +96,9 @@ public static FormElementGroup create() { } public String getDisplay() { - return display; + return getName(); } - public void setDisplay(String display) { - this.display = display; - } FormElement findFormElementByConcept(String conceptName) { return formElements.stream().filter(x -> x.getConcept().getName().equals(conceptName)).findAny().orElse(null); diff --git a/avni-server-api/src/main/java/org/avni/server/builder/FormBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/FormBuilder.java index 47f30a25c..20b0884e2 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/FormBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/FormBuilder.java @@ -94,7 +94,6 @@ public FormBuilder withFormElementGroups(List formElem .withBackgroundColour(formElementGroupContract.getBackgroundColour()) .withStartTime(formElementGroupContract.getStartTime()) .withStayTime(formElementGroupContract.getStayTime()) - .withDisplay(formElementGroupContract.getDisplay()) .withDisplayOrder(formElementGroupContract.getDisplayOrder()) .withRule(formElementGroupContract.getRule()) .withDeclarativeRule(formElementGroupContract.getDeclarativeRule()) diff --git a/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java index 99549591a..08c6cab76 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java @@ -45,11 +45,6 @@ public FormElementGroupBuilder withIsVoided(boolean isVoided) { return this; } - public FormElementGroupBuilder withDisplay(String display) { - this.set("Display", display, String.class); - return this; - } - public FormElementGroupBuilder withRule(String rule) { this.get().setRule(rule); return this; diff --git a/avni-server-api/src/main/java/org/avni/server/web/FormController.java b/avni-server-api/src/main/java/org/avni/server/web/FormController.java index a34d4baad..70f5d2b6d 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/FormController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/FormController.java @@ -298,7 +298,6 @@ public FormContract export(@RequestParam String formUUID) { form.getFormElementGroups().stream().sorted(Comparator.comparingDouble(FormElementGroup::getDisplayOrder)).forEach(formElementGroup -> { FormElementGroupContract formElementGroupContract = new FormElementGroupContract(formElementGroup.getUuid(), formElementGroup.getName(), formElementGroup.getDisplayOrder()); - formElementGroupContract.setDisplay(formElementGroup.getDisplay()); formElementGroupContract.setVoided(formElementGroup.isVoided()); formElementGroupContract.setOrganisationId(formElementGroup.getOrganisationId()); formElementGroupContract.setRule(formElementGroup.getRule()); diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/application/FormElementGroupContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/application/FormElementGroupContract.java index 56a0fc2af..0bc9fa924 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/application/FormElementGroupContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/application/FormElementGroupContract.java @@ -62,12 +62,9 @@ public void addFormElement(FormElementContract formElementContract) { } public String getDisplay() { - return display; + return getName(); } - public void setDisplay(String display) { - this.display = display; - } public DeclarativeRule getDeclarativeRule() { return declarativeRule; @@ -122,7 +119,6 @@ public String toString() { return "{" + "name=" + this.getName() + '\'' + "displayOrder=" + displayOrder + - ", display='" + display + '\'' + '}'; } @@ -138,7 +134,6 @@ public static FormElementGroupContract fromFormElementGroup(FormElementGroup feg FormElementGroupContract fegContract = new FormElementGroupContract(); fegContract.setName(feg.getName()); fegContract.setUuid(feg.getUuid()); - fegContract.setDisplay(feg.getDisplay()); fegContract.setDisplayOrder(feg.getDisplayOrder()); fegContract.setVoided(feg.isVoided()); fegContract.setRule(feg.getRule()); diff --git a/avni-server-api/src/main/resources/db/migration/V1_344__OverwriteFEGNameWithDisplayName.sql b/avni-server-api/src/main/resources/db/migration/V1_344__OverwriteFEGNameWithDisplayName.sql new file mode 100644 index 000000000..c4aa661d1 --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_344__OverwriteFEGNameWithDisplayName.sql @@ -0,0 +1,2 @@ +update form_element_group set name = display, last_modified_date_time = current_timestamp + random() * 5000 * (interval '1 millisecond') where display is not null and display <> '' and name <> display; +ALTER TABLE form_element_group DROP COLUMN display; diff --git a/avni-server-api/src/test/java/org/avni/server/application/TestFormElementGroupBuilder.java b/avni-server-api/src/test/java/org/avni/server/application/TestFormElementGroupBuilder.java index b3e0407d8..7f0c4bad5 100644 --- a/avni-server-api/src/test/java/org/avni/server/application/TestFormElementGroupBuilder.java +++ b/avni-server-api/src/test/java/org/avni/server/application/TestFormElementGroupBuilder.java @@ -33,11 +33,6 @@ public TestFormElementGroupBuilder withIsVoided(boolean isVoided) { return this; } - public TestFormElementGroupBuilder withDisplay(String display) { - formElementGroup.setDisplay(display); - return this; - } - public TestFormElementGroupBuilder withRule(String rule) { formElementGroup.setRule(rule); return this; From 66221cf9c07c05726d9227d887fd2a2d4f2d683d Mon Sep 17 00:00:00 2001 From: himeshr Date: Thu, 29 Aug 2024 12:43:18 +0530 Subject: [PATCH 125/129] avniproject/avni-webapp#1254 | Remove FEGContract.display field --- .../web/request/application/FormElementGroupContract.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/application/FormElementGroupContract.java b/avni-server-api/src/main/java/org/avni/server/web/request/application/FormElementGroupContract.java index 0bc9fa924..f6fe19f74 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/application/FormElementGroupContract.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/application/FormElementGroupContract.java @@ -13,8 +13,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class FormElementGroupContract extends ReferenceDataContract { private Double displayOrder; - private String display; - private List formElements = formElements = new ArrayList<>(); + private List formElements = new ArrayList<>(); private Long organisationId; private String rule; private DeclarativeRule declarativeRule; From 2134cc8a39d271efe129de1f1b2375242383b628 Mon Sep 17 00:00:00 2001 From: himeshr Date: Thu, 29 Aug 2024 14:46:58 +0530 Subject: [PATCH 126/129] avniproject/avni-webapp#1254 | Validate length of name for FEG and FE names --- .../org/avni/server/builder/FormElementBuilder.java | 6 ++++++ .../avni/server/builder/FormElementGroupBuilder.java | 12 +++++++++--- .../java/org/avni/server/web/FormController.java | 5 +++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/builder/FormElementBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/FormElementBuilder.java index 5cf221bd0..406fa54cf 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/FormElementBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/FormElementBuilder.java @@ -7,6 +7,9 @@ import org.avni.server.domain.Concept; import org.avni.server.domain.DeclarativeRule; import org.avni.server.domain.Documentation; +import org.springframework.util.StringUtils; + +import static java.lang.String.format; public class FormElementBuilder extends BaseBuilder { private final FormElementGroup formElementGroup; @@ -17,6 +20,9 @@ public FormElementBuilder(FormElementGroup formElementGroup, FormElement existin } public FormElementBuilder withName(String name) { + if(StringUtils.hasLength(name) && name.length() > 255) { + throw new BuilderException(format("FormElement name \"%s\" exceeds allowed length of 255 characters", name)); + } this.set("Name", name, String.class); return this; } diff --git a/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java index 08c6cab76..7d1eb917a 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java @@ -9,9 +9,12 @@ import org.avni.server.service.DocumentationService; import org.avni.server.web.request.application.FormElementContract; import org.avni.server.web.request.application.FormElementGroupContract; +import org.springframework.util.StringUtils; import java.util.List; +import static java.lang.String.format; + public class FormElementGroupBuilder extends BaseBuilder { private final ConceptService conceptService; private final DocumentationService documentationService; @@ -31,6 +34,9 @@ public FormElementGroupBuilder(Form form, FormElementGroup existingFormElementGr } public FormElementGroupBuilder withName(String name) { + if(StringUtils.hasLength(name) && name.length() > 255) { + throw new BuilderException(format("FormElementGroup name \"%s\" exceeds allowed length of 255 characters", name)); + } this.set("Name", name, String.class); return this; } @@ -90,7 +96,7 @@ private Concept getExistingConcept(String uuid, FormElement formElement) throws Concept concept = formElement.getConcept() != null && formElement.getConcept().getUuid().equals(uuid) ? formElement.getConcept() : conceptService.get(uuid); if (concept == null) { - throw new FormBuilderException(String.format("Concept with uuid '%s' not found", uuid)); + throw new FormBuilderException(format("Concept with uuid '%s' not found", uuid)); } return concept; } @@ -137,7 +143,7 @@ private FormElement getQuestionGroup(FormElementContract formElementContract) th if (formElementContract.getParentFormElementUuid() != null) { group = getExistingFormElement(formElementContract.getParentFormElementUuid()); if (group == null) { - throw new FormBuilderException(String.format("Parent form element with uuid '%s' not found", formElementContract.getParentFormElementUuid())); + throw new FormBuilderException(format("Parent form element with uuid '%s' not found", formElementContract.getParentFormElementUuid())); } } return group; @@ -159,7 +165,7 @@ public FormElementGroupBuilder withoutFormElements(Organisation organisation, Li private FormElement getFormElement(FormElementContract formElementContract) throws FormBuilderException { FormElement formElement = get().findFormElement(formElementContract.getUuid()); if (formElement == null) { - throw new FormBuilderException(String.format("FormElement with uuid '%s' not found", formElementContract.getUuid())); + throw new FormBuilderException(format("FormElement with uuid '%s' not found", formElementContract.getUuid())); } return formElement; } diff --git a/avni-server-api/src/main/java/org/avni/server/web/FormController.java b/avni-server-api/src/main/java/org/avni/server/web/FormController.java index 70f5d2b6d..c3e3a2119 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/FormController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/FormController.java @@ -1,6 +1,7 @@ package org.avni.server.web; import org.avni.server.application.*; +import org.avni.server.builder.BuilderException; import org.avni.server.builder.FormBuilder; import org.avni.server.builder.FormBuilderException; import org.avni.server.dao.OperationalProgramRepository; @@ -143,7 +144,7 @@ public ResponseEntity save(@RequestBody FormContract formRequest) { formRequest.validate(); formService.checkIfLocationConceptsHaveBeenUsed(formRequest); formService.saveForm(formRequest); - } catch (InvalidObjectException | FormBuilderException e) { + } catch (BuilderException | InvalidObjectException | FormBuilderException e) { logger.error(format("Error saving form: %s, with UUID: %s", formRequest.getName(), formRequest.getUuid()), e); return ResponseEntity.badRequest().body(errorBodyBuilder.getErrorMessageBody(e)); } @@ -236,7 +237,7 @@ public ResponseEntity patch(@RequestBody FormContract formRequest) { logger.info(format("Patching form: %s, with UUID: %s", formRequest.getName(), formRequest.getUuid())); try { formService.saveForm(formRequest); - } catch (FormBuilderException e) { + } catch (BuilderException | FormBuilderException e) { logger.info(format("Error patching form: %s, with UUID: %s", formRequest.getName(), formRequest.getUuid()), e); return ResponseEntity.badRequest().body(errorBodyBuilder.getErrorMessageBody(e)); } From 5309ddcda11227841e0661da24d51b7782f9e69d Mon Sep 17 00:00:00 2001 From: Joy A Date: Fri, 30 Aug 2024 17:07:32 +0530 Subject: [PATCH 127/129] avniproject/avni-webapp#1300 | GET /locations/* performance improvement --- .../avni/server/dao/LocationRepository.java | 28 ++++++++-------- .../server/service/AddressLevelService.java | 18 ++++++++++ .../avni/server/web/LocationController.java | 33 ++++++++++--------- .../web/request/AddressLevelContractWeb.java | 24 +++++++++++++- 4 files changed, 72 insertions(+), 31 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java b/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java index 0669aa305..adbd67de4 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/LocationRepository.java @@ -18,17 +18,19 @@ import javax.persistence.QueryHint; import javax.validation.constraints.NotNull; -import java.util.*; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Optional; @Repository @RepositoryRestResource(collectionResourceRel = "locations", path = "locations") public interface LocationRepository extends ReferenceDataRepository, FindByLastModifiedDateTime, OperatingIndividualScopeAwareRepository { @Query(value = "select al.id, al.uuid, title, type_id as typeId, alt.name as typeString, al.parent_id as parentId, " + - "cast(lineage as text) as lineage, title_lineage as titleLineage, alt.level " + + "cast(lineage as text) as lineage, null as titleLineage, alt.level " + "from address_level al " + "left join address_level_type alt on alt.id = al.type_id " + - "left join title_lineage_locations_view tll on tll.lowestpoint_id = al.id " + "where al.id in (:ids)", nativeQuery = true) List findByIdIn(Long[] ids); @@ -57,10 +59,9 @@ public interface LocationRepository extends ReferenceDataRepository findByTitleLineageIgnoreCase(String locationTitle String getTitleLineageById(Long addressId); @Query(value = "select al.id, al.uuid, title, type_id as typeId, alt.name as typeString, al.parent_id as parentId, " + - "cast(lineage as text) as lineage, title_lineage as titleLineage, alt.level " + + "cast(lineage as text) as lineage, " + + "null as titleLineage, " + + "alt.level " + "from address_level al " + "left join address_level_type alt on alt.id = al.type_id " + - "left join title_lineage_locations_view tll on tll.lowestpoint_id = al.id " + "where al.is_voided = false", nativeQuery = true) Page findNonVoidedLocations(Pageable pageable); @Query(value = "select al.id, al.uuid, title, type_id as typeId, alt.name as typeString, al.parent_id as parentId, " + - "cast(lineage as text) as lineage, title_lineage as titleLineage, alt.level " + + "cast(lineage as text) as lineage, null as titleLineage, alt.level " + "from address_level al " + "left join address_level_type alt on alt.id = al.type_id " + - "left join title_lineage_locations_view tll on tll.lowestpoint_id = al.id " + "where al.is_voided = false " + "and al.type_id = :typeId " + "order by al.title ", @@ -226,20 +227,18 @@ default Optional findByTitleLineageIgnoreCase(String locationTitle List findNonVoidedLocationsByTypeId(Long typeId); @Query(value = "select al.id, al.uuid, title, type_id as typeId, alt.name as typeString, al.parent_id as parentId, " + - "cast(lineage as text) as lineage, title_lineage as titleLineage, alt.level " + + "cast(lineage as text) as lineage, null as titleLineage, alt.level " + "from address_level al " + "left join address_level_type alt on alt.id = al.type_id " + - "left join title_lineage_locations_view tll on tll.lowestpoint_id = al.id " + "where al.is_voided = false " + "and al.uuid = :uuid ", nativeQuery = true) LocationProjection findNonVoidedLocationsByUuid(String uuid); @Query(value = "select al.id, al.uuid, title, type_id as typeId, alt.name as typeString, al.parent_id as parentId,\n" + - "cast(lineage as text) as lineage, title_lineage as titleLineage, alt.level " + + "cast(lineage as text) as lineage, null as titleLineage, alt.level " + "from address_level al\n" + "left join address_level_type alt on alt.id = al.type_id\n" + - "left join title_lineage_locations_view tll on tll.lowestpoint_id = al.id " + "where lineage @>" + " (select lineage" + " from address_level" + @@ -248,10 +247,9 @@ default Optional findByTitleLineageIgnoreCase(String locationTitle List getParentsWithMaxLevelTypeId(String uuid, Long maxLevelTypeId); @Query(value = "select al.id, al.uuid, title, type_id as typeId, alt.name as typeString, al.parent_id as parentId,\n" + - "cast(lineage as text) as lineage, title_lineage as titleLineage, alt.level " + + "cast(lineage as text) as lineage, null as titleLineage, alt.level " + "from address_level al\n" + "left join address_level_type alt on alt.id = al.type_id\n" + - "left join title_lineage_locations_view tll on tll.lowestpoint_id = al.id " + "where lineage @>" + " (select lineage" + " from address_level" + diff --git a/avni-server-api/src/main/java/org/avni/server/service/AddressLevelService.java b/avni-server-api/src/main/java/org/avni/server/service/AddressLevelService.java index 2fe04d147..f58bde08d 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/AddressLevelService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/AddressLevelService.java @@ -3,13 +3,17 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.avni.server.application.KeyType; +import org.avni.server.application.projections.LocationProjection; import org.avni.server.application.projections.VirtualCatchmentProjection; import org.avni.server.dao.AddressLevelTypeRepository; import org.avni.server.dao.LocationRepository; import org.avni.server.domain.*; import org.avni.server.util.ObjectMapperSingleton; import org.avni.server.web.request.AddressLevelContract; +import org.avni.server.web.request.AddressLevelContractWeb; import org.avni.server.web.request.webapp.SubjectTypeSetting; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.stereotype.Service; import java.util.*; @@ -98,6 +102,20 @@ public String getTitleLineage(AddressLevel location) { return locationRepository.getTitleLineageById(location.getId()); } + public List addTitleLineageToLocation(List locationProjections) { + Map titleLineages = getTitleLineages(locationProjections.stream().map(LocationProjection::getId).collect(Collectors.toList())); + return locationProjections.stream().map(locationProjection -> { + AddressLevelContractWeb addressLevel = AddressLevelContractWeb.fromEntity(locationProjection); + addressLevel.setTitleLineage(titleLineages.get(locationProjection.getId())); + return addressLevel; + }).collect(Collectors.toList()); + } + + public Page addTitleLineageToLocation(Page locationProjections) { + List locationWebContracts = addTitleLineageToLocation(locationProjections.getContent()); + return new PageImpl<>(locationWebContracts, locationProjections.getPageable(), locationProjections.getTotalElements()); + } + // This method uses in memory approach instead of database, because for smaller number of addresses the query plan to achive this is expensive due to over estimation by postgres. public Map getTitleLineages(List addressIds) { List addresses = locationRepository.findAllByIdIn(addressIds); diff --git a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java index 4978cc9c6..d3f3b4253 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java @@ -8,6 +8,7 @@ import org.avni.server.dao.sync.SyncEntityName; import org.avni.server.domain.AddressLevel; import org.avni.server.domain.accessControl.PrivilegeType; +import org.avni.server.service.AddressLevelService; import org.avni.server.service.LocationService; import org.avni.server.service.ScopeBasedSyncService; import org.avni.server.service.UserService; @@ -34,8 +35,8 @@ import javax.transaction.Transactional; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; @RepositoryRestController public class LocationController implements RestControllerResourceProcessor { @@ -47,15 +48,17 @@ public class LocationController implements RestControllerResourceProcessor scopeBasedSyncService; private final AccessControlService accessControlService; private final LocationSyncRepository locationSyncRepository; + private final AddressLevelService addressLevelService; @Autowired - public LocationController(LocationRepository locationRepository, UserService userService, LocationService locationService, ScopeBasedSyncService scopeBasedSyncService, AccessControlService accessControlService, LocationSyncRepository locationSyncRepository) { + public LocationController(LocationRepository locationRepository, UserService userService, LocationService locationService, ScopeBasedSyncService scopeBasedSyncService, AccessControlService accessControlService, LocationSyncRepository locationSyncRepository, AddressLevelService addressLevelService) { this.locationRepository = locationRepository; this.userService = userService; this.locationService = locationService; this.scopeBasedSyncService = scopeBasedSyncService; this.accessControlService = accessControlService; this.locationSyncRepository = locationSyncRepository; + this.addressLevelService = addressLevelService; this.logger = LoggerFactory.getLogger(this.getClass()); } @@ -77,18 +80,18 @@ public ResponseEntity save(@RequestBody List locationContra @GetMapping(value = "/locations") @ResponseBody - public Page getAll(Pageable pageable) { - return locationRepository.findNonVoidedLocations(pageable); + public Page getAll(Pageable pageable) { + return addressLevelService.addTitleLineageToLocation(locationRepository.findNonVoidedLocations(pageable)); } @GetMapping(value = "locations/search/find") @ResponseBody - public Page find( + public Page find( @RequestParam(value = "title", defaultValue = "") String title, @RequestParam(value = "typeId", required = false) Integer typeId, @RequestParam(value = "parentId", required = false) Integer parentId, Pageable pageable) { - return locationService.find(new LocationSearchRequest(title, typeId, parentId, pageable)); + return addressLevelService.addTitleLineageToLocation(locationService.find(new LocationSearchRequest(title, typeId, parentId, pageable))); } @GetMapping(value = "locations/search/findAsList") @@ -101,11 +104,11 @@ public List findAsList( @GetMapping(value = "/locations/search/findAllById") @ResponseBody - public List findByIdIn(@Param("ids") Long[] ids) { + public List findByIdIn(@Param("ids") Long[] ids) { if (ids == null || ids.length == 0) { return new ArrayList<>(); } - return locationRepository.findByIdIn(ids); + return addressLevelService.addTitleLineageToLocation(locationRepository.findByIdIn(ids)); } @RequestMapping(value = {"/locations/search/lastModified", "/locations/search/byCatchmentAndLastModified"}, method = RequestMethod.GET) @@ -157,27 +160,27 @@ public ResponseEntity voidLocation(@PathVariable("id") Long id) { @GetMapping(value = "/locations/search/typeId/{typeId}") @ResponseBody public List getLocationsByTypeId(@PathVariable("typeId") Long typeId) { + //TODO API does not appear to be in use. Remove. accessControlService.checkPrivilege(PrivilegeType.EditLocation); - return locationRepository.findNonVoidedLocationsByTypeId(typeId).stream() - .map(AddressLevelContractWeb::fromEntity) - .collect(Collectors.toList()); + return addressLevelService.addTitleLineageToLocation(locationRepository.findNonVoidedLocationsByTypeId(typeId)); } @GetMapping(value = "locations/parents/{uuid}") @ResponseBody - public List getParents(@PathVariable("uuid") String uuid, + public List getParents(@PathVariable("uuid") String uuid, @RequestParam(value = "maxLevelTypeId", required = false) Long maxLevelTypeId) { - return locationService.getParents(uuid, maxLevelTypeId); + return addressLevelService.addTitleLineageToLocation(locationService.getParents(uuid, maxLevelTypeId)); } @GetMapping(value = "/locations/web") @ResponseBody - public ResponseEntity getLocationByParam(@RequestParam("uuid") String uuid) { + public ResponseEntity getLocationByParam(@RequestParam("uuid") String uuid) { LocationProjection addressLevel = locationRepository.findNonVoidedLocationsByUuid(uuid); if (addressLevel == null) { return ResponseEntity.notFound().build(); } - return new ResponseEntity<>(AddressLevelContractWeb.fromEntity(addressLevel), HttpStatus.OK); + AddressLevelContractWeb addressLevelContract = addressLevelService.addTitleLineageToLocation(Collections.singletonList(addressLevel)).get(0); + return new ResponseEntity<>(addressLevelContract, HttpStatus.OK); } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/request/AddressLevelContractWeb.java b/avni-server-api/src/main/java/org/avni/server/web/request/AddressLevelContractWeb.java index 4950606a2..a487f41e7 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/request/AddressLevelContractWeb.java +++ b/avni-server-api/src/main/java/org/avni/server/web/request/AddressLevelContractWeb.java @@ -7,7 +7,9 @@ public class AddressLevelContractWeb { private Long id; private String name; + private String title; // set to same value as name to handle different consumers of contract private String type; + private String typeString; // set to same value as type to handle different consumers of contract private Long typeId; private Double level; private String lineage; @@ -19,7 +21,9 @@ public static AddressLevelContractWeb fromEntity(AddressLevel addressLevel) { AddressLevelContractWeb addressLevelContractWeb = new AddressLevelContractWeb(); addressLevelContractWeb.setId(addressLevel.getId()); addressLevelContractWeb.setName(addressLevel.getTitle()); - addressLevelContractWeb.setType(addressLevel.getType().getName()); + addressLevelContractWeb.setTitle(addressLevel.getTitle()); + addressLevelContractWeb.setType(addressLevel.getTypeString()); + addressLevelContractWeb.setTypeString(addressLevel.getTypeString()); addressLevelContractWeb.setTypeId(addressLevel.getType().getId()); addressLevelContractWeb.setLevel(addressLevel.getLevel()); addressLevelContractWeb.setParentId(addressLevel.getParentId()); @@ -32,7 +36,9 @@ public static AddressLevelContractWeb fromEntity(LocationProjection locationProj AddressLevelContractWeb addressLevelContractWeb = new AddressLevelContractWeb(); addressLevelContractWeb.setId(locationProjection.getId()); addressLevelContractWeb.setName(locationProjection.getTitle()); + addressLevelContractWeb.setTitle(locationProjection.getTitle()); addressLevelContractWeb.setType(locationProjection.getTypeString()); + addressLevelContractWeb.setTypeString(locationProjection.getTypeString()); addressLevelContractWeb.setTypeId(locationProjection.getTypeId()); addressLevelContractWeb.setLevel(locationProjection.getLevel()); addressLevelContractWeb.setParentId(locationProjection.getParentId()); @@ -113,4 +119,20 @@ public Long getTypeId() { public void setTypeId(Long typeId) { this.typeId = typeId; } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getTypeString() { + return typeString; + } + + public void setTypeString(String typeString) { + this.typeString = typeString; + } } From 0401bab851f4d316ed3858100feccb16c54fe631 Mon Sep 17 00:00:00 2001 From: Joy A Date: Mon, 2 Sep 2024 11:09:07 +0530 Subject: [PATCH 128/129] avniproject/avni-webapp#1300 | /web/subjectAssignment/search performance improvement - removed join to title_lineage_locations_view --- .../dao/search/SubjectAssignmentSearchQueryBuilder.java | 3 +-- .../avni/server/service/UserSubjectAssignmentService.java | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/avni-server-api/src/main/java/org/avni/server/dao/search/SubjectAssignmentSearchQueryBuilder.java b/avni-server-api/src/main/java/org/avni/server/dao/search/SubjectAssignmentSearchQueryBuilder.java index 4564ef2c2..d392c3c62 100644 --- a/avni-server-api/src/main/java/org/avni/server/dao/search/SubjectAssignmentSearchQueryBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/dao/search/SubjectAssignmentSearchQueryBuilder.java @@ -15,12 +15,11 @@ public SqlQuery build() { String SUBJECT_ASSIGNMENT_SEARCH_BASE_QUERY = "select i.id as \"id\",\n" + " cast(concat_ws(' ', i.first_name, i.middle_name, i.last_name) as text) as \"fullName\",\n" + " i.uuid as \"uuid\",\n" + - " cast(tllv.title_lineage as text) as \"addressLevel\",\n" + + " i.address_id as \"addressId\",\n" + " string_agg(distinct p.name || ':' || p.colour, ', ') as \"programs\",\n" + " string_agg(distinct assigned_to.name || ':' || g.name, ', ') as \"assignedTo\"\n" + " $CUSTOM_FIELDS\n" + "from individual i\n" + - " left outer join title_lineage_locations_view tllv on i.address_id = tllv.lowestpoint_id\n" + " left outer join subject_type st on i.subject_type_id = st.id and st.is_voided is false\n" + " left outer join program_enrolment penr on i.id = penr.individual_id and penr.is_voided is false\n" + " left outer join program p on p.id = penr.program_id\n" + diff --git a/avni-server-api/src/main/java/org/avni/server/service/UserSubjectAssignmentService.java b/avni-server-api/src/main/java/org/avni/server/service/UserSubjectAssignmentService.java index 319831c06..c1f9dc180 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/UserSubjectAssignmentService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/UserSubjectAssignmentService.java @@ -106,7 +106,10 @@ public LinkedHashMap searchSubjects(SubjectSearchRequest subject List> searchResults = subjectSearchRepository.search(subjectSearchRequest, new SubjectAssignmentSearchQueryBuilder()); List subjectIds = searchResults.stream().map(s -> Long.parseLong(s.get("id").toString())).collect(Collectors.toList()); List userSubjectAssignmentBySubjectIds = userSubjectAssignmentRepository.findUserSubjectAssignmentBySubject_IdIn(subjectIds); - + List addressIds = searchResults.stream() + .map(searchResult -> ((BigInteger) searchResult.get("addressId")).longValue()) + .collect(Collectors.toList()); + Map titleLineages = addressLevelService.getTitleLineages(addressIds); Map> groupedSubjects = userSubjectAssignmentBySubjectIds.stream() .filter(usa -> !usa.isVoided()) .collect(groupingBy(UserSubjectAssignment::getSubjectIdAsString, TreeMap::new, @@ -120,6 +123,7 @@ public LinkedHashMap searchSubjects(SubjectSearchRequest subject .orElseGet(Stream::empty) .map(uw -> pf.createProjection(UserWebProjection.class, uw)).collect(Collectors.toList()); searchResult.put("assignedUsers", userWebProjections); + searchResult.put("addressLevel", titleLineages.get(((BigInteger) searchResult.get("addressId")).longValue())); } BigInteger totalCount = subjectSearchRepository.getTotalCount(subjectSearchRequest, new SubjectAssignmentSearchQueryBuilder()); From b65c4e7f0300c49a229352fe88f334f34d983eff Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Wed, 4 Sep 2024 15:08:31 +0530 Subject: [PATCH 129/129] avniproject/avni-webapp#1314 - maintain bundle error message separately. ReactAdminUtil to use only user message. --- .../avni/server/builder/BuilderException.java | 20 ++++++++++++++ .../server/builder/FormElementBuilder.java | 2 +- .../builder/FormElementGroupBuilder.java | 2 +- .../avni/server/builder/LocationBuilder.java | 2 +- .../batch/csv/writer/BulkLocationCreator.java | 3 +-- .../batch/zip/BundleZipFileImporter.java | 5 ++-- .../avni/server/service/CatchmentService.java | 4 +-- .../avni/server/service/ConceptService.java | 2 +- .../avni/server/service/LocationService.java | 13 ++++----- .../org/avni/server/util/ReactAdminUtil.java | 7 ++++- .../avni/server/web/LocationController.java | 2 +- .../server/builder/BuilderExceptionTest.java | 27 +++++++++++++++++++ 12 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 avni-server-api/src/test/java/org/avni/server/builder/BuilderExceptionTest.java diff --git a/avni-server-api/src/main/java/org/avni/server/builder/BuilderException.java b/avni-server-api/src/main/java/org/avni/server/builder/BuilderException.java index 885c3f4cc..98337194d 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/BuilderException.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/BuilderException.java @@ -1,7 +1,27 @@ package org.avni.server.builder; public class BuilderException extends RuntimeException { + private final String bundleSpecificMessage; + public BuilderException(String message) { super(message); + bundleSpecificMessage = null; + } + + public BuilderException(String message, String bundleMessage) { + super(message); + this.bundleSpecificMessage = bundleMessage; + } + + public String getUserMessage() { + return super.getMessage(); + } + + @Override + public String getMessage() { + if (bundleSpecificMessage == null) { + return super.getMessage(); + } + return String.format("%s (%s)", super.getMessage(), bundleSpecificMessage); } } diff --git a/avni-server-api/src/main/java/org/avni/server/builder/FormElementBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/FormElementBuilder.java index 406fa54cf..bf29129d9 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/FormElementBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/FormElementBuilder.java @@ -20,7 +20,7 @@ public FormElementBuilder(FormElementGroup formElementGroup, FormElement existin } public FormElementBuilder withName(String name) { - if(StringUtils.hasLength(name) && name.length() > 255) { + if (StringUtils.hasLength(name) && name.length() > 255) { throw new BuilderException(format("FormElement name \"%s\" exceeds allowed length of 255 characters", name)); } this.set("Name", name, String.class); diff --git a/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java index 7d1eb917a..3ddd42c30 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/FormElementGroupBuilder.java @@ -34,7 +34,7 @@ public FormElementGroupBuilder(Form form, FormElementGroup existingFormElementGr } public FormElementGroupBuilder withName(String name) { - if(StringUtils.hasLength(name) && name.length() > 255) { + if (StringUtils.hasLength(name) && name.length() > 255) { throw new BuilderException(format("FormElementGroup name \"%s\" exceeds allowed length of 255 characters", name)); } this.set("Name", name, String.class); diff --git a/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java b/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java index 2ad7cfd19..5f856d3ea 100644 --- a/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java +++ b/avni-server-api/src/main/java/org/avni/server/builder/LocationBuilder.java @@ -21,7 +21,7 @@ public LocationBuilder(AddressLevel existingEntity, AddressLevelType type) { locationRepository = ApplicationContextProvider.getContext().getBean(LocationRepository.class); } - public LocationBuilder copy(LocationContract locationRequest) throws BuilderException { + public LocationBuilder copy(LocationContract locationRequest) { get().setUuid(locationRequest.getUuid() == null ? UUID.randomUUID().toString() : locationRequest.getUuid()); get().setTitle(locationRequest.getName()); get().setType(type); diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java index e505e241f..838c6230e 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/BulkLocationCreator.java @@ -2,7 +2,6 @@ import com.google.common.collect.Sets; import org.avni.server.application.FormElement; -import org.avni.server.builder.BuilderException; import org.avni.server.dao.AddressLevelTypeRepository; import org.avni.server.dao.LocationRepository; import org.avni.server.domain.AddressLevel; @@ -104,7 +103,7 @@ private void checkIfHeaderRowHasUnknownHeaders(List additionalHeaders, L } } - private AddressLevel createAddressLevel(Row row, AddressLevel parent, String header, List locationTypeNames) throws BuilderException { + private AddressLevel createAddressLevel(Row row, AddressLevel parent, String header, List locationTypeNames) { AddressLevel location; location = locationRepository.findChildLocation(parent, row.get(header)); if (location == null) { diff --git a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java index 42772c6e2..81473b89b 100644 --- a/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java +++ b/avni-server-api/src/main/java/org/avni/server/importer/batch/zip/BundleZipFileImporter.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.avni.messaging.contract.MessageRuleContract; import org.avni.messaging.service.MessagingService; -import org.avni.server.builder.BuilderException; import org.avni.server.builder.FormBuilderException; import org.avni.server.dao.CardRepository; import org.avni.server.dao.SubjectTypeRepository; @@ -249,7 +248,7 @@ private void deployFolder(BundleFolder bundleFolder, List } } - private void deployFile(String fileName, String fileData, List bundleFiles) throws IOException, FormBuilderException, BuilderException, ValidationException { + private void deployFile(String fileName, String fileData, List bundleFiles) throws IOException { logger.info("processing file {}", fileName); Organisation organisation = UserContextHolder.getUserContext().getOrganisation(); switch (fileName) { @@ -376,7 +375,7 @@ private void deployFile(String fileName, String fileData, List fileData, List bundleFiles) throws IOException, FormBuilderException, BuilderException { + private void deployFile(BundleFolder bundleFolder, Map.Entry fileData, List bundleFiles) throws IOException, FormBuilderException { logger.info("processing folder {} file {}", bundleFolder.getModifiedFileName(), fileData.getKey()); Organisation organisation = UserContextHolder.getUserContext().getOrganisation(); switch (bundleFolder) { diff --git a/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java b/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java index 89fa44a71..8753c61a6 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/CatchmentService.java @@ -53,7 +53,7 @@ public Catchment createOrUpdate(String catchmentName, AddressLevel location) { return catchmentRepository.save(catchment); } - public List saveAllCatchments(CatchmentsContract catchmentsContract, Organisation organisation) throws BuilderException { + public List saveAllCatchments(CatchmentsContract catchmentsContract, Organisation organisation) { List catchments = new ArrayList<>(); for (CatchmentContract catchmentRequest : catchmentsContract.getCatchments()) { logger.info(String.format("Processing catchment request: %s", catchmentRequest.toString())); @@ -79,7 +79,7 @@ public List saveAllCatchments(CatchmentsContract catchmentsContract, return catchments; } - private void addAddressLevels(CatchmentContract catchmentRequest, Catchment catchment) throws BuilderException { + private void addAddressLevels(CatchmentContract catchmentRequest, Catchment catchment) { List locations = catchmentRequest.getLocations(); if(isNull(locations) || locations.isEmpty()) { logger.warn(String.format("Locations not defined in Catchment {uuid='%s',locations=undefined,...}", catchment.getUuid())); diff --git a/avni-server-api/src/main/java/org/avni/server/service/ConceptService.java b/avni-server-api/src/main/java/org/avni/server/service/ConceptService.java index 3fd28fdfb..2b93bf17c 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ConceptService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ConceptService.java @@ -178,7 +178,7 @@ private Concept saveOrUpdate(ConceptContract conceptRequest) throws AnswerConcep if (conceptExistsWithSameNameAndDifferentUUID(conceptRequest)) { throw new BadRequestError(String.format("Concept with name \'%s\' already exists", conceptRequest.getName())); } - logger.info(String.format("Creating concept: %s", conceptRequest.toString())); + logger.info(String.format("Creating concept: %s", conceptRequest)); addToMigrationIfRequired(conceptRequest); Concept concept = map(conceptRequest); diff --git a/avni-server-api/src/main/java/org/avni/server/service/LocationService.java b/avni-server-api/src/main/java/org/avni/server/service/LocationService.java index 1e31dd0ea..93b239ea0 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/LocationService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/LocationService.java @@ -54,7 +54,7 @@ public LocationService(LocationRepository locationRepository, AddressLevelTypeRe this.logger = LoggerFactory.getLogger(this.getClass()); } - public List saveAll(List locationContracts) throws BuilderException { + public List saveAll(List locationContracts) { List saved = new ArrayList<>(); for (LocationContract contract : locationContracts) { saved.add(save(contract)); @@ -62,7 +62,7 @@ public List saveAll(List locationContracts) thro return saved; } - public AddressLevel save(LocationContract locationContract) throws BuilderException { + public AddressLevel save(LocationContract locationContract) { logger.info(String.format("Processing location request: %s", locationContract.toString())); AddressLevelType type = getTypeByUuidOrName(locationContract); if (type == null) { @@ -98,7 +98,7 @@ private AddressLevelType createType(LocationContract locationContract) { return addressLevelType; } - private AddressLevel saveLocation(LocationContract contract, AddressLevelType type) throws BuilderException { + private AddressLevel saveLocation(LocationContract contract, AddressLevelType type) { LocationBuilder locationBuilder = new LocationBuilder(locationRepository.findByUuid(contract.getUuid()), type); locationBuilder.copy(contract); AddressLevel location = locationBuilder.build(); @@ -106,20 +106,21 @@ private AddressLevel saveLocation(LocationContract contract, AddressLevelType ty // Validate location title/name is unique only if new AddressLevel if (location.getId() == null && !titleIsValid(location, contract.getName().trim(), type)) - throw new BuilderException(String.format("Location with same name '%s' and type '%s' exists at this level. (%s)", contract.getName(), type.getName(), contract)); + throw new BuilderException(String.format("Location with same name '%s' and type '%s' exists at this level.", contract.getName(), type.getName()), + contract.toString()); try { locationRepository.save(location); } catch (Exception e) { logger.error(e.getMessage(), e); - throw new BuilderException(String.format("Unable to create Location{name='%s',level='%s',orgUUID='%s',..}: '%s' (%s)", contract.getName(), contract.getLevel(), contract.getOrganisationUUID(), e.getMessage(), contract)); + throw new BuilderException(String.format("Unable to create Location{name='%s',level='%s',orgUUID='%s',..}: '%s'", contract.getName(), contract.getLevel(), contract.getOrganisationUUID(), e.getMessage()), contract.toString()); } try { location.calculateLineage(); locationRepository.save(location); } catch (Exception e) { logger.error(e.getMessage(), e); - throw new BuilderException(String.format("Unable to update lineage for location with Id %s - %s. (%s)", location.getId(), e.getMessage(), contract)); + throw new BuilderException(String.format("Unable to update lineage for location with Id %s - %s.", location.getId(), e.getMessage()), contract.toString()); } return location; } diff --git a/avni-server-api/src/main/java/org/avni/server/util/ReactAdminUtil.java b/avni-server-api/src/main/java/org/avni/server/util/ReactAdminUtil.java index 333d6c4c6..9cd568eb9 100644 --- a/avni-server-api/src/main/java/org/avni/server/util/ReactAdminUtil.java +++ b/avni-server-api/src/main/java/org/avni/server/util/ReactAdminUtil.java @@ -1,9 +1,15 @@ package org.avni.server.util; +import org.avni.server.builder.BuilderException; + import java.util.HashMap; import java.util.Map; public class ReactAdminUtil { + public static Map generateJsonError(BuilderException builderException) { + return generateJsonError(builderException.getUserMessage()); + } + public static Map generateJsonError(String errorMsg) { Map errorMap = new HashMap<>(); errorMap.put("message", errorMsg); @@ -13,5 +19,4 @@ public static Map generateJsonError(String errorMsg) { public static String getVoidedName(String name, Long id) { return String.format("%s (voided~%d)", name, id); } - } diff --git a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java index d3f3b4253..20a1dd821 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/LocationController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/LocationController.java @@ -73,7 +73,7 @@ public ResponseEntity save(@RequestBody List locationContra } } catch (BuilderException e) { logger.error(e.getMessage(), e); - return ResponseEntity.badRequest().body(ReactAdminUtil.generateJsonError(e.getMessage())); + return ResponseEntity.badRequest().body(ReactAdminUtil.generateJsonError(e)); } return ResponseEntity.ok(null); } diff --git a/avni-server-api/src/test/java/org/avni/server/builder/BuilderExceptionTest.java b/avni-server-api/src/test/java/org/avni/server/builder/BuilderExceptionTest.java new file mode 100644 index 000000000..51c43ca31 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/builder/BuilderExceptionTest.java @@ -0,0 +1,27 @@ +package org.avni.server.builder; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class BuilderExceptionTest { + @Test + public void hasBundleSpecificMessage() { + try { + throw new BuilderException("userMessage", "bundleSpecificMessage"); + } catch (BuilderException be) { + assertEquals("userMessage (bundleSpecificMessage)", be.getMessage()); + assertEquals("userMessage", be.getUserMessage()); + } + } + + @Test + public void doesntHaveBundleSpecificMessage() { + try { + throw new BuilderException("userMessage"); + } catch (BuilderException be) { + assertEquals("userMessage", be.getMessage()); + assertEquals("userMessage", be.getUserMessage()); + } + } +}