Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
/** @format */

module.exports = {
extends: [
"react-app",
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
],
extends: ["react-app", "eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
rules: {
"no-console": ["warn", { allow: ["debug", "warn", "error"] }],
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ The Bulk Load application generates templates (an Excel sheet) and imports multi

- Settings are only visible for superusers (`ALL` authority) or users that belong to the settings groups.
- The generation box is only visible for users that belong to the configurable `Template Generation` groups (initial value: `HMIS Officers`).
- Imported files are stored as documents (and can be downloaded later from the history) only for users with the `ALL` or `F_DOCUMENT_PRIVATE_ADD` authority. Users without them see a warning in the import page: their data is imported as usual and the import is still recorded in the history, but without the file.

## Setup

Expand Down
17 changes: 15 additions & 2 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2026-07-29T11:52:52.301Z\n"
"PO-Revision-Date: 2026-07-29T11:52:52.301Z\n"
"POT-Creation-Date: 2026-08-19T14:35:59.584Z\n"
"PO-Revision-Date: 2026-08-19T14:35:59.584Z\n"

msgid "Events - Create/update"
msgstr ""
Expand Down Expand Up @@ -222,6 +222,9 @@ msgstr ""
msgid "Import strategy"
msgstr ""

msgid "Org. Unit"
msgstr ""

msgid "File"
msgstr ""

Expand All @@ -231,6 +234,9 @@ msgstr ""
msgid "The original file is not available for download"
msgstr ""

msgid "and {{total}} more"
msgstr ""

msgid "Default"
msgstr ""

Expand Down Expand Up @@ -1477,6 +1483,13 @@ msgstr ""
msgid "Bulk data import"
msgstr ""

msgid ""
"The data will be imported as usual, but the file itself will not be saved "
"because you do not have any of the required authorities ({{authorities}}). "
"The import will still be recorded in the history, without a file available "
"for download."
msgstr ""

msgid "Drag and drop file to import"
msgstr ""

Expand Down
15 changes: 14 additions & 1 deletion i18n/es.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Bulk Load\n"
"POT-Creation-Date: 2026-07-29T11:52:52.301Z\n"
"POT-Creation-Date: 2026-08-19T14:35:59.584Z\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -245,6 +245,9 @@ msgstr "Estado"
msgid "Import strategy"
msgstr "Importar datos"

msgid "Org. Unit"
msgstr "Unidad organizativa"

msgid "File"
msgstr ""

Expand All @@ -255,6 +258,9 @@ msgstr ""
msgid "The original file is not available for download"
msgstr ""

msgid "and {{total}} more"
msgstr "y {{total}} más"

msgid "Default"
msgstr ""

Expand Down Expand Up @@ -1566,6 +1572,13 @@ msgstr "Descargar datos con unidades organizativas no válidas"
msgid "Bulk data import"
msgstr "Importación de datos"

msgid ""
"The data will be imported as usual, but the file itself will not be saved "
"because you do not have any of the required authorities ({{authorities}}). "
"The import will still be recorded in the history, without a file available "
"for download."
msgstr ""

msgid "Drag and drop file to import"
msgstr "Mueva aquí los ficheros a importar"

Expand Down
15 changes: 14 additions & 1 deletion i18n/fr.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Bulk Load App\n"
"POT-Creation-Date: 2026-07-29T11:52:52.301Z\n"
"POT-Creation-Date: 2026-08-19T14:35:59.584Z\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -239,6 +239,9 @@ msgstr ""
msgid "Import strategy"
msgstr "Importer des données"

msgid "Org. Unit"
msgstr ""

msgid "File"
msgstr ""

Expand All @@ -249,6 +252,9 @@ msgstr ""
msgid "The original file is not available for download"
msgstr ""

msgid "and {{total}} more"
msgstr ""

msgid "Default"
msgstr ""

Expand Down Expand Up @@ -1588,6 +1594,13 @@ msgstr ""
msgid "Bulk data import"
msgstr "Importation de données en masse"

msgid ""
"The data will be imported as usual, but the file itself will not be saved "
"because you do not have any of the required authorities ({{authorities}}). "
"The import will still be recorded in the history, without a file available "
"for download."
msgstr ""

msgid "Drag and drop file to import"
msgstr "Faites glisser et déposez le fichier à importer ici"

Expand Down
15 changes: 14 additions & 1 deletion i18n/pt.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Bulk Load\n"
"POT-Creation-Date: 2026-07-29T11:52:52.301Z\n"
"POT-Creation-Date: 2026-08-19T14:35:59.584Z\n"
"Language: pt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -247,6 +247,9 @@ msgstr "Estado"
msgid "Import strategy"
msgstr "Importar dados"

msgid "Org. Unit"
msgstr ""

msgid "File"
msgstr ""

Expand All @@ -257,6 +260,9 @@ msgstr ""
msgid "The original file is not available for download"
msgstr ""

msgid "and {{total}} more"
msgstr ""

msgid "Default"
msgstr ""

Expand Down Expand Up @@ -1610,6 +1616,13 @@ msgstr "Descarregar dados com unidades organizacionais inválidas"
msgid "Bulk data import"
msgstr "Importação de dados em massa"

msgid ""
"The data will be imported as usual, but the file itself will not be saved "
"because you do not have any of the required authorities ({{authorities}}). "
"The import will still be recorded in the history, without a file available "
"for download."
msgstr ""

msgid "Drag and drop file to import"
msgstr "Arraste e solte o arquivo para importar"

Expand Down
16 changes: 15 additions & 1 deletion i18n/ru.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Bulk Load\n"
"POT-Creation-Date: 2026-07-29T11:52:52.301Z\n"
"POT-Creation-Date: 2026-08-19T14:35:59.584Z\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -251,6 +251,10 @@ msgstr "Статус"
msgid "Import strategy"
msgstr "Импортные данные"

#, fuzzy
msgid "Org. Unit"
msgstr "Оргподразделение *"

msgid "File"
msgstr ""

Expand All @@ -261,6 +265,9 @@ msgstr ""
msgid "The original file is not available for download"
msgstr ""

msgid "and {{total}} more"
msgstr ""

msgid "Default"
msgstr ""

Expand Down Expand Up @@ -1617,6 +1624,13 @@ msgstr "Загрузка значений данных с недопустимы
msgid "Bulk data import"
msgstr "Массовый импорт данных"

msgid ""
"The data will be imported as usual, but the file itself will not be saved "
"because you do not have any of the required authorities ({{authorities}}). "
"The import will still be recorded in the history, without a file available "
"for download."
msgstr ""

msgid "Drag and drop file to import"
msgstr "Перетащите файл для импорта"

Expand Down
5 changes: 5 additions & 0 deletions src/CompositionRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ import { ModulesRepositories } from "./domain/repositories/ModulesRepositories";
import { ImportSourceNodeRepository } from "./data/ImportSourceNodeRepository";
import { DataElementDisaggregationsMappingD2Repository } from "./data/DataElementDisaggregationsMappingD2Repository";
import { GetDataElementDisaggregationsMappingUseCase } from "./domain/usecases/GetDataElementDisaggregationsMappingUseCase";
import { OrgUnitD2Repository } from "./data/OrgUnitD2Repository";
import { OrgUnitRepository } from "./domain/repositories/OrgUnitRepository";
import { GetOrgUnitsByIdsUseCase } from "./domain/usecases/GetOrgUnitsByIdsUseCase";

export interface CompositionRootOptions {
appConfig: JsonConfig;
Expand Down Expand Up @@ -87,13 +90,15 @@ export function getCompositionRoot({ appConfig, dhisInstance, mockApi, importSou
const importSourceRepository =
importSource === "zip" ? new ImportSourceZipRepository() : new ImportSourceNodeRepository();
const historyRepository: HistoryRepository = new HistoryDataStoreRepository(dhisInstance, mockApi);
const orgUnitRepository: OrgUnitRepository = new OrgUnitD2Repository(dhisInstance, mockApi);

const dataElementDisaggregationsMappingRepository = new DataElementDisaggregationsMappingD2Repository(api);

return {
orgUnits: getExecute({
getUserRoots: new GetOrgUnitRootsUseCase(instance),
getRootsByForm: new GetFormOrgUnitRootsUseCase(instance),
getByIds: new GetOrgUnitsByIdsUseCase(orgUnitRepository),
}),
form: getExecute({
getDataPackage: new GetFormDataPackageUseCase(instance),
Expand Down
6 changes: 5 additions & 1 deletion src/data/D2UsersRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DhisInstance } from "../domain/entities/DhisInstance";
import { User } from "../domain/entities/User";
import { SearchResults, UsersRepository } from "../domain/repositories/UsersRepository";
import { D2OrganisationUnit, D2Api } from "../types/d2-api";
import { canUploadDocuments } from "./d2-authorities";

export class D2UsersRepository implements UsersRepository {
private api: D2Api;
Expand Down Expand Up @@ -36,11 +37,14 @@ export class D2UsersRepository implements UsersRepository {
})
.getData();

const authorities = new Set(apiUser.authorities);

return {
id: apiUser.id,
name: apiUser.name,
username: apiUser.username ?? apiUser.userCredentials?.username ?? "",
authorities: new Set(apiUser.authorities),
authorities,
canUploadDocuments: canUploadDocuments(authorities),
userGroups: apiUser.userGroups,
orgUnitsView: apiUser.dataViewOrganisationUnits.map(this.buildOrgUnit),
orgUnits: apiUser.organisationUnits.map(this.buildOrgUnit),
Expand Down
37 changes: 37 additions & 0 deletions src/data/OrgUnitD2Repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import _ from "lodash";
import { DhisInstance } from "../domain/entities/DhisInstance";
import { OrgUnit } from "../domain/entities/OrgUnit";
import { Id } from "../domain/entities/ReferenceObject";
import { OrgUnitRepository } from "../domain/repositories/OrgUnitRepository";
import { D2Api } from "../types/d2-api";
import { promiseMap } from "../utils/promises";

// Avoid 414-uri-too-large by splitting ids in chunks
const CHUNK_SIZE = 250;

export class OrgUnitD2Repository implements OrgUnitRepository {
private api: D2Api;

constructor({ url }: DhisInstance, mockApi?: D2Api) {
this.api = mockApi ?? new D2Api({ baseUrl: url });
}

/* Org units that are deleted or not accessible to the current user are simply not returned */
public async getByIds(ids: Id[]): Promise<OrgUnit[]> {
if (_.isEmpty(ids)) return [];

const orgUnitsByChunk = await promiseMap(_.chunk(ids, CHUNK_SIZE), async chunkIds => {
const { objects } = await this.api.models.organisationUnits
.get({
paging: false,
fields: { id: true, displayName: true, level: true, path: true },
filter: { id: { in: chunkIds } },
})
.getData();

return objects.map(({ displayName, ...orgUnit }) => ({ ...orgUnit, name: displayName }));
});

return _.flatten(orgUnitsByChunk);
}
}
8 changes: 8 additions & 0 deletions src/data/d2-authorities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* Authorities are a DHIS2 concept: they are translated here into the capabilities that the rest
of the app uses, so that no other layer has to know which authority grants what. */

export const documentUploadAuthorities = ["ALL", "F_DOCUMENT_PRIVATE_ADD"] as const;

export function canUploadDocuments(authorities: ReadonlySet<string>): boolean {
return documentUploadAuthorities.some(authority => authorities.has(authority));
}
Loading
Loading