From 3734b8d1588ea13f92accbb291916f687aa76128 Mon Sep 17 00:00:00 2001 From: xurxodev Date: Wed, 18 Mar 2026 13:12:44 +0100 Subject: [PATCH 1/3] Fix org unit validation on template download - Only require organisation unit selection when downloading populated tracker templates. - This prevents dataset template downloads from being blocked when org unit selection is configured for import-only, and defaults relationship OU filter to CAPTURE in that mode. --- src/webapp/components/template-selector/TemplateSelector.tsx | 2 +- src/webapp/pages/download-template/DownloadTemplatePage.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/webapp/components/template-selector/TemplateSelector.tsx b/src/webapp/components/template-selector/TemplateSelector.tsx index 9fc4a918..ecc755dd 100644 --- a/src/webapp/components/template-selector/TemplateSelector.tsx +++ b/src/webapp/components/template-selector/TemplateSelector.tsx @@ -77,7 +77,7 @@ export const TemplateSelector = ({ { startDate: moment().add("-1", "year").startOf("year"), endDate: moment().add("-1", "year").endOf("year"), - relationshipsOuFilter: "SELECTED", + relationshipsOuFilter: settings.orgUnitSelection === "import" ? "CAPTURE" : "SELECTED", populate: false, downloadRelationships: true, filterTEIEnrollmentDate: false, diff --git a/src/webapp/pages/download-template/DownloadTemplatePage.tsx b/src/webapp/pages/download-template/DownloadTemplatePage.tsx index f983004f..ec629730 100644 --- a/src/webapp/pages/download-template/DownloadTemplatePage.tsx +++ b/src/webapp/pages/download-template/DownloadTemplatePage.tsx @@ -30,6 +30,8 @@ export default function DownloadTemplatePage({ settings, themes, customTemplates template; if ( + type === "trackerPrograms" && + populate && (template.relationshipsOuFilter === "SELECTED" || template.relationshipsOuFilter === "CHILDREN" || template.relationshipsOuFilter === "DESCENDANTS") && From 5a4dd51090efae2232ed88aab92e8e6e1198effb Mon Sep 17 00:00:00 2001 From: xurxodev Date: Thu, 11 Jun 2026 11:13:23 +0200 Subject: [PATCH 2/3] fix: empty org unit selector in downloaded template when org unit selection is set to import When orgUnitSelection setting is "import", the org unit selector is hidden on the download page, so no org units are sent to the download use case. This left the org unit selector empty in the generated spreadsheet. Now it falls back to the intersection of the current user's capture org units and the org units of the selected dataset/program. --- src/domain/usecases/DownloadTemplateUseCase.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/domain/usecases/DownloadTemplateUseCase.ts b/src/domain/usecases/DownloadTemplateUseCase.ts index b4f06e86..f9d68711 100644 --- a/src/domain/usecases/DownloadTemplateUseCase.ts +++ b/src/domain/usecases/DownloadTemplateUseCase.ts @@ -95,12 +95,20 @@ export class DownloadTemplateUseCase implements UseCase { const element = await getElement(api, type, id); const name = element.displayName ?? element.name; + const orgUnitIds = + _.isEmpty(orgUnits) && settings.orgUnitSelection === "import" + ? _.intersection( + currentUser.orgUnits.map(orgUnit => orgUnit.id), + element.organisationUnits.map((orgUnit: Ref) => orgUnit.id) + ) + : orgUnits; + async function getGenerateFile(maxTeiRows?: number) { const result = await getElementMetadata({ api, element, downloadRelationships, - orgUnitIds: orgUnits, + orgUnitIds, startDate: startDate?.toDate(), endDate: endDate?.toDate(), populateStartDate: populateStartDate?.toDate(), From cde526a6b5dee2bbced60e744f02419f2ac20ea0 Mon Sep 17 00:00:00 2001 From: xurxodev Date: Thu, 11 Jun 2026 13:27:58 +0200 Subject: [PATCH 3/3] fix: include user's capture org units (or descendants of them) in downloaded template when org unit selection is set to import --- .../usecases/DownloadTemplateUseCase.ts | 6 +- .../usecases/utils/__tests__/orgUnits.spec.ts | 67 +++++++++++++++++++ src/domain/usecases/utils/orgUnits.ts | 19 ++++++ 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 src/domain/usecases/utils/__tests__/orgUnits.spec.ts create mode 100644 src/domain/usecases/utils/orgUnits.ts diff --git a/src/domain/usecases/DownloadTemplateUseCase.ts b/src/domain/usecases/DownloadTemplateUseCase.ts index f9d68711..bda3db39 100644 --- a/src/domain/usecases/DownloadTemplateUseCase.ts +++ b/src/domain/usecases/DownloadTemplateUseCase.ts @@ -21,6 +21,7 @@ import { UsersRepository } from "../repositories/UsersRepository"; import { buildAllPossiblePeriods } from "../../webapp/utils/periods"; import { applyFilter } from "../entities/TemplateFilter"; import { DataElementDisaggregationsMappingRepository } from "../repositories/DataElementDisaggregationsMappingRepository"; +import { getCaptureOrgUnitIdsForDataForm } from "./utils/orgUnits"; export interface DownloadTemplateProps { type: DataFormType; @@ -97,10 +98,7 @@ export class DownloadTemplateUseCase implements UseCase { const orgUnitIds = _.isEmpty(orgUnits) && settings.orgUnitSelection === "import" - ? _.intersection( - currentUser.orgUnits.map(orgUnit => orgUnit.id), - element.organisationUnits.map((orgUnit: Ref) => orgUnit.id) - ) + ? getCaptureOrgUnitIdsForDataForm(element.organisationUnits, currentUser.orgUnits) : orgUnits; async function getGenerateFile(maxTeiRows?: number) { diff --git a/src/domain/usecases/utils/__tests__/orgUnits.spec.ts b/src/domain/usecases/utils/__tests__/orgUnits.spec.ts new file mode 100644 index 00000000..50dc6a33 --- /dev/null +++ b/src/domain/usecases/utils/__tests__/orgUnits.spec.ts @@ -0,0 +1,67 @@ +import { OrgUnit } from "../../../entities/OrgUnit"; +import { getCaptureOrgUnitIdsForDataForm } from "../orgUnits"; + +function buildOrgUnit(id: string, path: string): OrgUnit { + return { id, path, name: id, level: path.split("/").length - 1 }; +} + +describe("getCaptureOrgUnitIdsForDataForm", () => { + it("returns empty when the data form org unit is a parent of the user org units", () => { + const dataFormOrgUnits = [{ id: "global", path: "/global" }]; + const userOrgUnits = [buildOrgUnit("spain", "/global/spain"), buildOrgUnit("albania", "/global/albania")]; + + const result = getCaptureOrgUnitIdsForDataForm(dataFormOrgUnits, userOrgUnits); + + expect(result).toEqual([]); + }); + + it("returns the data form org units when the user has a parent org unit", () => { + const dataFormOrgUnits = [ + { id: "spain", path: "/global/spain" }, + { id: "albania", path: "/global/albania" }, + ]; + const userOrgUnits = [buildOrgUnit("global", "/global")]; + + const result = getCaptureOrgUnitIdsForDataForm(dataFormOrgUnits, userOrgUnits); + + expect(result).toEqual(["spain", "albania"]); + }); + + it("returns the data form org unit when the user has the exact same org unit", () => { + const dataFormOrgUnits = [{ id: "spain", path: "/global/spain" }]; + const userOrgUnits = [buildOrgUnit("spain", "/global/spain")]; + + const result = getCaptureOrgUnitIdsForDataForm(dataFormOrgUnits, userOrgUnits); + + expect(result).toEqual(["spain"]); + }); + + it("only returns the data form org units the user has access to", () => { + const dataFormOrgUnits = [ + { id: "spain", path: "/global/spain" }, + { id: "france", path: "/global/france" }, + ]; + const userOrgUnits = [buildOrgUnit("spain", "/global/spain")]; + + const result = getCaptureOrgUnitIdsForDataForm(dataFormOrgUnits, userOrgUnits); + + expect(result).toEqual(["spain"]); + }); + + it("does not match org units with similar path prefixes that are not ancestors", () => { + const dataFormOrgUnits = [{ id: "spain2", path: "/global/spain2" }]; + const userOrgUnits = [buildOrgUnit("spain", "/global/spain")]; + + const result = getCaptureOrgUnitIdsForDataForm(dataFormOrgUnits, userOrgUnits); + + expect(result).toEqual([]); + }); + + it("returns empty when the user has no org units", () => { + const dataFormOrgUnits = [{ id: "spain", path: "/global/spain" }]; + + const result = getCaptureOrgUnitIdsForDataForm(dataFormOrgUnits, []); + + expect(result).toEqual([]); + }); +}); diff --git a/src/domain/usecases/utils/orgUnits.ts b/src/domain/usecases/utils/orgUnits.ts new file mode 100644 index 00000000..aba4be59 --- /dev/null +++ b/src/domain/usecases/utils/orgUnits.ts @@ -0,0 +1,19 @@ +import { OrgUnit } from "../../entities/OrgUnit"; +import { Id, Ref } from "../../entities/ReferenceObject"; + +/* Return the org units of the data form (dataSet/program) the current user can capture data for, + either directly or through a parent org unit (matched by path). */ +export function getCaptureOrgUnitIdsForDataForm( + dataFormOrgUnits: Array, + userOrgUnits: OrgUnit[] +): Id[] { + return dataFormOrgUnits + .filter(dataFormOrgUnit => + userOrgUnits.some( + userOrgUnit => + dataFormOrgUnit.path === userOrgUnit.path || + dataFormOrgUnit.path.startsWith(`${userOrgUnit.path}/`) + ) + ) + .map(orgUnit => orgUnit.id); +}