Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Export CSV] Export data stored in new proto-based format #1917

Merged
merged 66 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
83514d5
Rename var
gino-m Jul 17, 2024
4442b78
Add tsdoc
gino-m Jul 17, 2024
d68f2a7
Refactor getSubmissionsByLoi
gino-m Jul 17, 2024
0c5fd9a
Refactor write fn and add necessary types
gino-m Jul 17, 2024
8562bca
Add missing return type
gino-m Jul 17, 2024
e8500b0
Simplify imports
gino-m Jul 17, 2024
a854999
Simplify loop
gino-m Jul 17, 2024
fb3aa3d
Minor renames
gino-m Jul 17, 2024
0f256ee
Convert protos to GeoJSON
gino-m Jul 17, 2024
de28a3d
Export functions
gino-m Jul 17, 2024
e161f78
Rename file, fix types
gino-m Jul 17, 2024
6bc1dcf
Add email address back into audit info
gino-m Jul 17, 2024
d3e5be2
Export data in new format
gino-m Jul 17, 2024
495a708
Merge branch 'master' of https://github.com/google/ground-platform in…
gino-m Jul 17, 2024
c738251
Fix typo
gino-m Jul 18, 2024
e9b2cd0
Merge branch 'master' of https://github.com/google/ground-platform in…
gino-m Jul 18, 2024
e428df2
Test WIP
gino-m Jul 18, 2024
9474ec4
Merge branch 'master' of https://github.com/google/ground-platform in…
gino-m Jul 31, 2024
6af1118
Merge branch 'master' of https://github.com/google/ground-platform in…
gino-m Jul 31, 2024
845f9f2
Set up response testing
gino-m Jul 31, 2024
d367bd5
Remove TODO
gino-m Jul 31, 2024
e6b47ef
Add test for property maps
gino-m Jul 31, 2024
5c4b00c
Fix test data
gino-m Jul 31, 2024
2bdf354
Quote columns, remove legacy support
gino-m Jul 31, 2024
daa54bd
Test multiple LOIs, fix null properties
gino-m Jul 31, 2024
a1edb1a
Start adding tests for submissions
gino-m Jul 31, 2024
97fa652
Add method to get field ids for a proto message class
gino-m Jul 31, 2024
29900bd
Test field registry
gino-m Aug 1, 2024
834a7af
Use field registry
gino-m Aug 1, 2024
82e3c21
Fix id
gino-m Aug 1, 2024
64b8bd8
format tsconfig
gino-m Aug 1, 2024
17c69b4
Replace aliases with registry
gino-m Aug 1, 2024
f0abc7b
Replace field numbers with registry
gino-m Aug 1, 2024
56cfd0d
Add CR
gino-m Aug 1, 2024
cf2bc9d
Remove redundant exclude
gino-m Aug 1, 2024
64886fa
Remove remaining ref to FieldNumbers
gino-m Aug 1, 2024
ba68c1b
removed FieldNumbers
rfontanarosa Aug 1, 2024
d73e4e9
Merge branch 'gino-m/patch/field-id-util' into gino-m/1779/export-csv…
gino-m Aug 1, 2024
0f421d0
Use registry for field nos
gino-m Aug 1, 2024
fd1b220
Rename proto packages to fix name collision with google/
gino-m Aug 2, 2024
d52c40a
Remove google/ from proto path
gino-m Aug 2, 2024
4cae528
Merge branch 'master' into gino-m/patch/field-id-util
gino-m Aug 2, 2024
4cd0c5c
Fix proto source path
gino-m Aug 2, 2024
d87aef9
Merge branch 'gino-m/patch/field-id-util' of https://github.com/googl…
gino-m Aug 2, 2024
ea74b67
Update proto include paths
gino-m Aug 2, 2024
de130b7
Merge branch 'gino-m/patch/field-id-util' into gino-m/1779/export-csv…
gino-m Aug 2, 2024
2bf5acf
Update namespace
gino-m Aug 2, 2024
87bab69
Merge branch 'master' of https://github.com/google/ground-platform in…
gino-m Aug 4, 2024
a4258e8
Fix `number` field number
gino-m Aug 5, 2024
d4125c0
Implement text, number, date-time export
gino-m Aug 5, 2024
550c7fb
Export multiple choice options
gino-m Aug 5, 2024
d81e701
Test capture location export
gino-m Aug 5, 2024
ff724c5
Add TODO
gino-m Aug 5, 2024
e7a38fc
Export photo URLs
gino-m Aug 5, 2024
b66c94e
Fix tests
gino-m Aug 5, 2024
a6811ad
Fix lint error
gino-m Aug 5, 2024
2a98373
Refactor writing submissions
gino-m Aug 5, 2024
7db19c8
Add clarifying comment
gino-m Aug 6, 2024
a5aaea9
Fix string formatting nit
gino-m Aug 6, 2024
55438f9
Use ?? to allow for empty strings
gino-m Aug 6, 2024
9ff1626
Simplify number value extraction
gino-m Aug 6, 2024
8c54d05
Use flatmap instead of map + flat
gino-m Aug 6, 2024
b614bbd
Use implicit undefined equality
gino-m Aug 6, 2024
ca8e429
Merge branch 'master' into gino-m/1779/export-csv-from-pb-data
gino-m Aug 6, 2024
3537692
Allow == null checks in linters
gino-m Aug 6, 2024
2206c63
Merge branch 'gino-m/1779/export-csv-from-pb-data' of https://github.…
gino-m Aug 6, 2024
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
3 changes: 2 additions & 1 deletion e2e-tests/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"rules": {
"node/no-unpublished-import": ["error", {
"allowModules": ["jasmine"]
}]
}],
"eqeqeq": ["error", "always", {"null": "ignore"}]
}
}
5 changes: 4 additions & 1 deletion functions/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
"parserOptions": {
"sourceType": "module"
},
"root": true
"root": true,
"rules": {
"eqeqeq": ["error", "always", {"null": "ignore"}]
}
}
292 changes: 292 additions & 0 deletions functions/src/export-csv.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
/**
* Copyright 2024 The Ground Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
createMockFirestore,
stubAdminApi,
} from '@ground/lib/dist/testing/firestore';
import {
createGetRequestSpy,
createResponseSpy,
} from './testing/http-test-helpers';
import {DecodedIdToken} from 'firebase-admin/auth';
import HttpStatus from 'http-status-codes';
import {OWNER_ROLE} from './common/auth';
import {resetDatastore} from './common/context';
import {Firestore} from 'firebase-admin/firestore';
import {exportCsvHandler} from './export-csv';
import {registry} from '@ground/lib';
import {GroundProtos} from '@ground/proto';

import Pb = GroundProtos.ground.v1beta1;
const l = registry.getFieldIds(Pb.LocationOfInterest);
const pr = registry.getFieldIds(Pb.LocationOfInterest.Property);
const p = registry.getFieldIds(Pb.Point);
const c = registry.getFieldIds(Pb.Coordinates);
const g = registry.getFieldIds(Pb.Geometry);
const s = registry.getFieldIds(Pb.Submission);
const d = registry.getFieldIds(Pb.TaskData);
const cl = registry.getFieldIds(Pb.TaskData.CaptureLocationResult);

describe('exportCsv()', () => {
let mockFirestore: Firestore;
const jobId = 'job123';
const email = '[email protected]';
const userId = 'user5000';
// TODO(#1758): Use new proto-based survey and job representation.
const survey1 = {
id: 'survey001',
name: 'Test survey 1',
acl: {
[email]: OWNER_ROLE,
},
};
const survey2 = {
id: 'survey002',
name: 'Test survey 2',
acl: {
[email]: OWNER_ROLE,
},
jobs: {
[jobId]: {
name: 'Test job',
tasks: {
task001: {
type: 'text_field',
label: 'What is the meaning of life?',
},
task002: {
type: 'number_field',
label: 'How much?',
},
task003: {
type: 'date_time_field',
label: 'When?',
},
task004: {
type: 'select_multiple',
label: 'Which ones?',
options: [
{
id: 'aaa',
label: 'AAA',
},
{
id: 'bbb',
label: 'BBB',
},
],
hasOtherOption: true,
},
task005: {
type: 'capture_location',
label: 'Where are you now?',
},
task006: {
type: 'take_photo',
label: 'Take a photo',
},
},
},
},
};
const pointLoi1 = {
id: 'loi100',
[l.jobId]: jobId,
[l.customTag]: 'POINT_001',
[l.geometry]: {
[g.point]: {[p.coordinates]: {[c.latitude]: 10.1, [c.longitude]: 125.6}},
},
[l.submission_count]: 0,
[l.source]: Pb.LocationOfInterest.Source.IMPORTED,
[l.properties]: {
name: {[pr.stringValue]: 'Dinagat Islands'},
area: {[pr.numericValue]: 3.08},
},
};
const pointLoi2 = {
id: 'loi200',
[l.jobId]: jobId,
[l.customTag]: 'POINT_002',
[l.geometry]: {
[g.point]: {[p.coordinates]: {[c.latitude]: 47.05, [c.longitude]: 8.3}},
},
[l.submissionCount]: 0,
[l.source]: Pb.LocationOfInterest.Source.FIELD_DATA,
[l.properties]: {
name: {[pr.stringValue]: 'Luzern'},
},
};
const submission1a = {
id: '001a',
[s.loiId]: pointLoi1.id,
[s.index]: 1,
[s.jobId]: jobId,
[s.ownerId]: userId,
[s.taskData]: [
{
[d.id]: 'data001a',
[d.taskId]: 'task001',
[d.textResponse]: {
'1': 'Submission 1',
},
},
{
[d.id]: 'data002a',
[d.taskId]: 'task002',
[d.numberResponse]: {
'1': 42,
},
},
],
};
const submission1b = {
id: '001b',
[s.loiId]: pointLoi1.id,
[s.index]: 2,
[s.jobId]: jobId,
[s.ownerId]: userId,
[s.taskData]: [
{
[d.id]: 'data001b',
[d.taskId]: 'task001',
[d.textResponse]: {
'1': 'Submission 2',
},
},
{
[d.id]: 'data003a',
[d.taskId]: 'task003',
[d.dateTimeResponse]: {
'1': {
'1': 1331209044, // seconds
},
},
},
],
};
const submission2a = {
id: '002a',
[s.loiId]: pointLoi2.id,
[s.index]: 1,
[s.jobId]: jobId,
[s.ownerId]: userId,
[s.taskData]: [
{
[d.id]: 'data004',
[d.taskId]: 'task004',
[d.multipleChoiceResponses]: {
'1': ['aaa', 'bbb'],
'2': 'Other',
},
},
{
[d.id]: 'data005a',
[d.taskId]: 'task005',
[d.captureLocationResult]: {
[cl.coordinates]: {
[c.latitude]: -123,
[c.longitude]: 45,
},
},
},
{
[d.id]: 'data006b',
[d.taskId]: 'task006',
[d.takePhotoResult]: {
'1': 'http://photo/url',
},
},
],
};
const testCases = [
{
desc: 'export points w/o submissions',
survey: survey1,
lois: [pointLoi1, pointLoi2],
submissions: [],
expectedFilename: 'ground-export.csv',
expectedCsv: [
'"system:index","geometry","name","area","data:contributor_name","data:contributor_email"',
'"POINT_001","POINT (125.6 10.1)","Dinagat Islands",3.08,,',
'"POINT_002","POINT (8.3 47.05)","Luzern",,,',
],
},
{
desc: 'export points w/submissions',
survey: survey2,
lois: [pointLoi1, pointLoi2],
submissions: [submission1a, submission1b, submission2a],
expectedFilename: 'test-job.csv',
expectedCsv: [
'"system:index","geometry","name","area","data:What is the meaning of life?","data:How much?","data:When?","data:Which ones?","data:Where are you now?","data:Take a photo","data:contributor_name","data:contributor_email"',
'"POINT_001","POINT (125.6 10.1)","Dinagat Islands",3.08,"Submission 1",42,,,,,,',
'"POINT_001","POINT (125.6 10.1)","Dinagat Islands",3.08,"Submission 2",,"2012-03-08T12:17:24.000Z",,,,,',
'"POINT_002","POINT (8.3 47.05)","Luzern",,,,,"AAA,BBB,Other","POINT (45 -123)","http://photo/url",,',
],
},
];

beforeEach(() => {
mockFirestore = createMockFirestore();
stubAdminApi(mockFirestore);
});

afterEach(() => {
resetDatastore();
});

testCases.forEach(
({desc, survey, lois, submissions, expectedFilename, expectedCsv}) =>
it(desc, async () => {
// Populate database.
mockFirestore.doc(`surveys/${survey.id}`).set(survey);
lois?.forEach(({id, ...loi}) =>
mockFirestore.doc(`surveys/${survey.id}/lois/${id}`).set(loi)
);
submissions?.forEach(({id, ...submission}) =>
mockFirestore
.doc(`surveys/${survey.id}/submissions/${id}`)
.set(submission)
);

// Build mock request and response.
const req = await createGetRequestSpy({
url: '/exportCsv',
query: {
survey: survey.id,
job: jobId,
},
});
const chunks: string[] = [];
const res = createResponseSpy(chunks);

// Run export CSV handler.
await exportCsvHandler(req, res, {email} as DecodedIdToken);

// Check post-conditions.
expect(res.status).toHaveBeenCalledOnceWith(HttpStatus.OK);
expect(res.type).toHaveBeenCalledOnceWith('text/csv');
expect(res.setHeader).toHaveBeenCalledOnceWith(
'Content-Disposition',
`attachment; filename=${expectedFilename}`
);
const output = chunks.join('').trim();
const lines = output.split('\n');
expect(lines).toEqual(expectedCsv);
})
);
});
Loading
Loading