Skip to content

Commit 6e5924a

Browse files
authored
Fix out of sync kobo fields (#272)
1 parent 11e5141 commit 6e5924a

13 files changed

Lines changed: 240 additions & 43 deletions

File tree

‎apps/backend/src/main.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AppModule } from './app.module';
66
import { Environment } from './env.validation';
77
import { CustomLogger } from './modules/logger/custom-logger.service';
88
import { getAllowedOriginMatchers } from './utils/allowedOrigins';
9+
import { configureQueryParser } from './utils/queryParser';
910

1011
const bootstrap = async () => {
1112
const logger = new CustomLogger();
@@ -15,6 +16,7 @@ const bootstrap = async () => {
1516
app.enableCors({ credentials: true, origin: getAllowedOriginMatchers() });
1617
}
1718

19+
configureQueryParser(app);
1820
app.use(cookieParser());
1921

2022
// Setting up Swagger document

‎apps/backend/src/modules/kobo/tests/kobo.controller.spec.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { INestApplication } from '@nestjs/common';
66
import { Test, TestingModule } from '@nestjs/testing';
77
import { getRepositoryToken } from '@nestjs/typeorm';
88
import { AppModule } from '@root/app.module';
9+
import { configureQueryParser } from '@root/utils/queryParser';
910
import {
1011
DisasterDtoType,
1112
DROUGHT,
@@ -45,6 +46,7 @@ describe('KoboController', () => {
4546
authService = testingModule.get(AuthService);
4647
koboService = testingModule.get(KoboService);
4748
app = testingModule.createNestApplication();
49+
configureQueryParser(app);
4850
await app.init();
4951
});
5052

@@ -182,6 +184,38 @@ describe('KoboController', () => {
182184
});
183185
});
184186
});
187+
188+
it('should accept more than 20 province values in axios bracket query format', async () => {
189+
const role = 'ncdm';
190+
const disTyps = ['1'];
191+
const startDate = '2026-08-01';
192+
const endDate = '2026-09-01';
193+
const province = Array.from({ length: 25 }, (_, index) => String(index + 1).padStart(2, '0'));
194+
195+
const getFormsSpy = jest.spyOn(koboService, 'getForms').mockImplementation(getFormsMock);
196+
197+
const user = await userFactory.createOne({ roles: [role] });
198+
const accessToken = authService.createAccessToken(user, 10000);
199+
const queryString = province.reduce(
200+
(params, code) => `${params}&province[]=${code}`,
201+
`disTyps[]=1&startDate=${startDate}&endDate=${endDate}`,
202+
);
203+
204+
await request(app.getHttpServer())
205+
.get(`/kobo/forms?${queryString}`)
206+
.set('Authorization', `Bearer ${accessToken}`)
207+
.expect(200)
208+
.expect(() => {
209+
expect(getFormsSpy).toHaveBeenNthCalledWith(1, {
210+
disTyps,
211+
startDate,
212+
endDate,
213+
province,
214+
district: undefined,
215+
commune: undefined,
216+
});
217+
});
218+
});
185219
});
186220

187221
describe('GET form', () => {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { INestApplication } from '@nestjs/common';
2+
import { NestExpressApplication } from '@nestjs/platform-express';
3+
import qs from 'qs';
4+
5+
// qs defaults arrayLimit to 20; admin-area filters can exceed that.
6+
const QUERY_ARRAY_LIMIT = 2000;
7+
8+
export const configureQueryParser = (app: INestApplication): void => {
9+
const expressApp = app as NestExpressApplication;
10+
11+
expressApp.set('query parser', (queryString: string) =>
12+
qs.parse(queryString, { arrayLimit: QUERY_ARRAY_LIMIT }),
13+
);
14+
};

‎apps/frontend/components/FormValidation/FloodFormValidation/tablesConfig/Agriculture.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ const AgricultureColumns: GridColDef[] = [
1919
getColumnSetup({
2020
field: FloodSpecific.NumFarmCroAff,
2121
disaster: FLOOD,
22+
width: colWidth + 8 * 4,
23+
}),
24+
getColumnSetup({
25+
field: FloodSpecific.NumFarmPaddyAff,
26+
disaster: FLOOD,
2227
width: colWidth + 8 * 2,
2328
}),
2429
getColumnSetup({
@@ -33,7 +38,6 @@ const AgricultureColumns: GridColDef[] = [
3338
}),
3439
getColumnSetup({ field: FloodSpecific.SamNabAff, disaster: FLOOD }),
3540
getColumnSetup({ field: FloodSpecific.SamNabDam, disaster: FLOOD }),
36-
getColumnSetup({ field: FloodSpecific.NumFarmPaddyAff, disaster: FLOOD }),
3741
getColumnSetup({ field: FloodSpecific.PaddyAff, disaster: FLOOD }),
3842
getColumnSetup({ field: FloodSpecific.PaddyDam, disaster: FLOOD }),
3943
getColumnSetup({ field: FloodSpecific.CowEva, disaster: FLOOD }),
@@ -53,6 +57,7 @@ const AgricultureColumnGroup: GridColumnGroupingModel = [
5357
children: [
5458
{ field: FloodSpecific.ToNamAgriAff },
5559
{ field: FloodSpecific.NumFarmCroAff },
60+
{ field: FloodSpecific.NumFarmPaddyAff },
5661
],
5762
},
5863
{
@@ -65,7 +70,6 @@ const AgricultureColumnGroup: GridColumnGroupingModel = [
6570
{
6671
...getGroupSetup('seedling', FLOOD),
6772
children: [
68-
{ field: FloodSpecific.NumFarmPaddyAff },
6973
{ field: FloodSpecific.SamNabAff },
7074
{ field: FloodSpecific.SamNabDam },
7175
],

‎apps/frontend/services/api/kobo/useGetForms.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { DroughtDto, FloodDto, IncidentDto } from '@wfp-dmp/interfaces';
1+
import {
2+
DroughtDto,
3+
FloodDto,
4+
IncidentDto,
5+
provinces,
6+
} from '@wfp-dmp/interfaces';
27
import useSWR from 'swr';
38

49
import { SearchFormData } from 'components/Filters/SearchFilters';
@@ -13,7 +18,10 @@ export const useGetForms = ({
1318
const inputStartDate = inputDateRange.startDate?.format('YYYY-MM-DD');
1419
const inputEndDate = inputDateRange.endDate?.format('YYYY-MM-DD');
1520
const inputProvince =
16-
inputRegion.province.length === 0 ? undefined : inputRegion.province;
21+
inputRegion.province.length === 0 ||
22+
inputRegion.province.length === provinces.length
23+
? undefined
24+
: inputRegion.province;
1725
const inputDistrict =
1826
inputRegion.district.length === 0 ? undefined : inputRegion.district;
1927
const inputCommune =

‎apps/frontend/translations/en.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,11 @@
231231
"NumWareHDam": "Damaged",
232232
"NumCraftAff": "Affected",
233233
"NumCraftDam": "Damaged",
234-
"ToNamAgriAff": "Affected",
235-
"NumFarmCroAff": "Affected",
234+
"ToNamAgriAff": "Farmland",
235+
"NumFarmCroAff": "Mixed crops",
236236
"FarmAff": "Affected",
237237
"FarmDam": "Damaged",
238-
"NumFarmPaddyAff": "Affected",
238+
"NumFarmPaddyAff": "Seedlings",
239239
"SamNabAff": "Affected",
240240
"SamNabDam": "Damaged",
241241
"PaddyAff": "Affected",

‎apps/frontend/translations/km.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,11 @@
235235
"NumWareHDam": "ខូចខាត",
236236
"NumCraftAff": "ប៉ះពាល់",
237237
"NumCraftDam": "ខូចខាត",
238-
"ToNamAgriAff": "ប៉ះពាល់",
239-
"NumFarmCroAff": "ប៉ះពាល់",
238+
"ToNamAgriAff": "ដីស្រែ/ចម្ការ",
239+
"NumFarmCroAff": "ដំណាំរួមផ្សំ",
240240
"FarmAff": "ប៉ះពាល់",
241241
"FarmDam": "ខូចខាត",
242-
"NumFarmPaddyAff": "ប៉ះពាល់",
242+
"NumFarmPaddyAff": "សំណាប/សន្ទូង",
243243
"SamNabAff": "ប៉ះពាល់",
244244
"SamNabDam": "ខូចខាត",
245245
"PaddyAff": "ប៉ះពាល់",
Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,73 @@
1-
import { FLOOD, floodSpecificKeys, koboKeys } from '@wfp-dmp/interfaces';
1+
import {
2+
FLOOD,
3+
floodSpecificKeys,
4+
INCIDENT,
5+
incidentSpecificKeys,
6+
koboKeys,
7+
} from '@wfp-dmp/interfaces';
28
import dayjs from 'dayjs';
39

410
import { FloodFormType } from 'components/FormValidation/FloodFormValidation/FloodFormType';
11+
import { IncidentFormType } from 'components/FormValidation/IncidentFormValidation/IncidentFormType';
512
import { formatFormToRaw } from 'utils/formatFormToRaw';
613

714
describe('formatFormToRaw', () => {
8-
const formValues = {
9-
region: {
10-
province: ['01'],
11-
district: ['0102'],
12-
commune: ['010203'],
13-
village: 'village',
14-
},
15-
interviewer: 'tester',
16-
disTyp: '1',
17-
phone: '012',
18-
reportDate: dayjs('2024-01-02'),
19-
incidentDate: dayjs('2024-01-01'),
20-
specific: {
21-
NumFamAff: 50,
22-
floodN: '3',
23-
},
24-
} as unknown as FloodFormType;
25-
2615
it('stringifies numeric DataGrid values and omits unmapped keys', () => {
16+
const formValues = {
17+
region: {
18+
province: ['01'],
19+
district: ['0102'],
20+
commune: ['010203'],
21+
village: 'village',
22+
},
23+
interviewer: 'tester',
24+
disTyp: '1',
25+
phone: '012',
26+
reportDate: dayjs('2024-01-02'),
27+
incidentDate: dayjs('2024-01-01'),
28+
specific: {
29+
NumFamAff: 50,
30+
floodN: '3',
31+
FarmAff: 31,
32+
},
33+
} as unknown as FloodFormType;
34+
2735
const raw = formatFormToRaw(formValues, koboKeys[FLOOD], floodSpecificKeys);
2836

2937
expect(raw['g3/g3_1/g3_2/NumFamAff']).toBe('50');
3038
expect(raw['g2/flood_n']).toBe('3');
39+
expect(raw['g11/Crop/FarmAff']).toBe('31');
40+
expect(raw).not.toHaveProperty('g11/g11_1/FarmAff');
3141
expect(raw).not.toHaveProperty('undefined');
3242
});
43+
44+
it('writes incident factory fields to current Kobo xpaths', () => {
45+
const incidentValues = {
46+
region: {
47+
province: ['05'],
48+
district: ['0503'],
49+
commune: ['050303'],
50+
village: 'village',
51+
},
52+
interviewer: 'tester',
53+
disTyp: '11',
54+
phone: '012',
55+
reportDate: dayjs('2024-01-02'),
56+
incidentDate: dayjs('2024-01-01'),
57+
specific: {
58+
FactoryAff: 7,
59+
FactoryDam: 4,
60+
},
61+
} as unknown as IncidentFormType;
62+
63+
const raw = formatFormToRaw(
64+
incidentValues,
65+
koboKeys[INCIDENT],
66+
incidentSpecificKeys,
67+
);
68+
69+
expect(raw['group_pk03i04/group_az7bv13/FactoryAff']).toBe('7');
70+
expect(raw['group_pk03i04/group_az7bv13/FactoryDam']).toBe('4');
71+
expect(raw).not.toHaveProperty('group_gh6ag70/group_az7bv13/FactoryAff');
72+
});
3373
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { FloodDto, IncidentDto } from '@wfp-dmp/interfaces';
2+
3+
import { formatFloodFields, formatIncidentFields } from 'utils/formatRawToForm';
4+
5+
const baseFlood = {
6+
_id: 816716609,
7+
'formhub/uuid': 'uid',
8+
start: '2026-09-03T00:00:00.000Z',
9+
end: '2026-09-03T00:00:00.000Z',
10+
'g1/q_Enum': 'admin',
11+
'g1/q_Funtion': 'flood_admin',
12+
'g1/q_Phone': '12345678',
13+
'g1/Date_report': '2026-09-03',
14+
'g2/Province': '24',
15+
'g2/District': '2401',
16+
'g2/Commune': '240103',
17+
'g2/Date_Dis': '2026-09-03',
18+
'g2/DisTyp': '1',
19+
__version__: 'version',
20+
'meta/instanceID': 'uuid',
21+
_xform_id_string: 'ids',
22+
_uuid: 'uid',
23+
_attachments: [],
24+
_status: 'submitted_via_web',
25+
_geolocation: [null, null],
26+
_submission_time: '2026-09-03T08:00:00',
27+
_tags: [],
28+
_notes: [],
29+
_validation_status: {},
30+
_submitted_by: null,
31+
} as FloodDto;
32+
33+
describe('formatFloodFields agriculture mapping', () => {
34+
it('maps current Kobo agriculture xpaths into review fields', () => {
35+
const formatted = formatFloodFields({
36+
...baseFlood,
37+
'g11/ToNamAgriAff': '33',
38+
'g11/NumFarmCroAff': '32',
39+
'g11/Crop/FarmAff': '31',
40+
'g11/Crop/FarmDam': '30',
41+
'g11/NumFarmPaddyAff': '29',
42+
'g11/g11_2/SamNabAff': '28',
43+
} as FloodDto);
44+
45+
expect(formatted.ToNamAgriAff).toBe('33');
46+
expect(formatted.NumFarmCroAff).toBe('32');
47+
expect(formatted.FarmAff).toBe('31');
48+
expect(formatted.FarmDam).toBe('30');
49+
expect(formatted.NumFarmPaddyAff).toBe('29');
50+
expect(formatted.SamNabAff).toBe('28');
51+
});
52+
});
53+
54+
const baseIncident = {
55+
_id: 3,
56+
'formhub/uuid': 'uid',
57+
start: '2026-09-03T00:00:00.000Z',
58+
end: '2026-09-03T00:00:00.000Z',
59+
'G1/q_Enum': 'admin',
60+
'G1/q_Funtion': 'admin_incident',
61+
'G1/q_Phone': '12345678',
62+
'G1/Date_report': '2026-09-03',
63+
'G2/Province': '05',
64+
'G2/District': '0503',
65+
'G2/Commune': '050303',
66+
'G2/Date_Dis': '2026-09-03',
67+
'G2/DisTyp': '11',
68+
__version__: 'version',
69+
'meta/instanceID': 'uuid',
70+
_xform_id_string: 'ids',
71+
_uuid: 'uid',
72+
_attachments: [],
73+
_status: 'submitted_via_web',
74+
_geolocation: [null, null],
75+
_submission_time: '2026-09-03T08:00:00',
76+
_tags: [],
77+
_notes: [],
78+
_validation_status: {},
79+
_submitted_by: null,
80+
} as IncidentDto;
81+
82+
describe('formatIncidentFields factory mapping', () => {
83+
it('maps current Kobo factory xpaths into review fields', () => {
84+
const formatted = formatIncidentFields({
85+
...baseIncident,
86+
'group_pk03i04/group_az7bv13/FactoryAff': '7',
87+
'group_pk03i04/group_az7bv13/FactoryDam': '4',
88+
'group_gh6ag70/group_na4yi04/CropAff': '2',
89+
} as IncidentDto);
90+
91+
expect(formatted.FactoryAff).toBe('7');
92+
expect(formatted.FactoryDam).toBe('4');
93+
expect(formatted.CropAff).toBe('2');
94+
});
95+
});

‎packages/interfaces/src/kobo/FloodDto.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ export class FloodDto {
8484
@IsOptional() @IsNumberString() readonly 'g10/g10_7/NumWareHDam'?: string;
8585
@IsOptional() @IsNumberString() readonly 'g10/g10_8/NumCraftAff'?: string;
8686
@IsOptional() @IsNumberString() readonly 'g10/g10_8/NumCraftDam'?: string;
87-
@IsOptional() @IsNumberString() readonly 'g11/g11_1/FarmAff'?: string;
88-
@IsOptional() @IsNumberString() readonly 'g11/g11_1/FarmDam'?: string;
89-
@IsOptional() @IsNumberString() readonly 'g11/g11_1/ToNamAgriAff'?: string;
90-
@IsOptional() @IsNumberString() readonly 'g11/g11_1/NumFarmCroAff'?: string;
91-
@IsOptional() @IsNumberString() readonly 'g11/g11_2/NumFarmPaddyAff'?: string;
87+
@IsOptional() @IsNumberString() readonly 'g11/ToNamAgriAff'?: string;
88+
@IsOptional() @IsNumberString() readonly 'g11/NumFarmCroAff'?: string;
89+
@IsOptional() @IsNumberString() readonly 'g11/Crop/FarmAff'?: string;
90+
@IsOptional() @IsNumberString() readonly 'g11/Crop/FarmDam'?: string;
91+
@IsOptional() @IsNumberString() readonly 'g11/NumFarmPaddyAff'?: string;
9292
@IsOptional() @IsNumberString() readonly 'g11/g11_2/SamNabAff'?: string;
9393
@IsOptional() @IsNumberString() readonly 'g11/g11_2/SamNabDam'?: string;
9494
@IsOptional() @IsNumberString() readonly 'g11/g11_3/PaddyAff'?: string;

0 commit comments

Comments
 (0)