Skip to content

Commit b501144

Browse files
committed
Allow blank file attachments in bulk upload
This commit allows for a bulk upload to proceed when a cell is blank for a file. This was a requested feature for the service by @grant-minor-sntialtech ! Issue #2429 Allow null or blank file attachments for some rows in bulk upload
1 parent 64ea5d7 commit b501144

4 files changed

Lines changed: 128 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Fixed
11+
12+
- Bulk upload processing no longer fails when a row leaves a file-typed field blank. The empty cell is stored as a non-file value (`value: ""`, `isValid: false`) and no attachment lookup is attempted for that field.
13+
1014
### Changed
1115

1216
- Creating an entity now automatically grants the creator a `manage` permission with `any` scope on the new entity. This applies to opportunities, changemakers, proposals, sources, bulk upload tasks, application forms (and their fields), proposal versions (and their field values), and changemaker field values created via the HTTP API, as well as proposals, proposal versions, proposal field values, and newly inserted changemakers created during bulk upload processing.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
proposal_submitter_email,organization_name,favorite_file
2+
foo@example.com,Foo LLC.,one.txt
3+
foo@example.com,Bar Inc.,
4+
foo@example.com,Baz Co.," "
5+
foo@example.com,Qux Ltd.," one.txt "

src/tasks/__tests__/processBulkUploadTask.int.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,117 @@ describe('processBulkUploadTask', () => {
748748
expect(updatedBulkUploadTask.status).toEqual(TaskStatus.COMPLETED);
749749
});
750750

751+
it('should process rows with blank or whitespace-only file attachment cells without failing the bulk upload', async () => {
752+
const db = getDatabase();
753+
await createTestBaseFields(db);
754+
const testAuthContext = await getTestAuthContext(db);
755+
const systemUser = await loadSystemUser(db, null);
756+
const systemUserAuthContext = getAuthContext(systemUser);
757+
const proposalsDataFile = await createTestFile(db, systemUserAuthContext);
758+
const attachmentsArchiveFile = await createTestFile(
759+
db,
760+
systemUserAuthContext,
761+
);
762+
const { applicationFormId } = await createTestApplicationForm(
763+
db,
764+
systemUserAuthContext,
765+
['proposal_submitter_email', 'organization_name', 'favorite_file'],
766+
);
767+
const bulkUploadTask = await createTestBulkUploadTask(
768+
db,
769+
systemUserAuthContext,
770+
{
771+
proposalsDataFileId: proposalsDataFile.id,
772+
applicationFormId,
773+
overrideValues: {
774+
attachmentsArchiveFileId: attachmentsArchiveFile.id,
775+
},
776+
},
777+
);
778+
779+
s3Mock
780+
.on(GetObjectCommand, { Key: proposalsDataFile.storageKey })
781+
.resolves({
782+
Body: sdkStreamMixin(
783+
fs.createReadStream(
784+
path.join(
785+
__dirname,
786+
'fixtures',
787+
'processBulkUploadTask',
788+
'validCsvTemplateWithBlankFile.csv',
789+
),
790+
),
791+
),
792+
});
793+
794+
s3Mock
795+
.on(GetObjectCommand, { Key: attachmentsArchiveFile.storageKey })
796+
.resolves({
797+
Body: sdkStreamMixin(
798+
fs.createReadStream(
799+
path.join(
800+
__dirname,
801+
'fixtures',
802+
'processBulkUploadTask',
803+
'attachments.zip',
804+
),
805+
),
806+
),
807+
});
808+
809+
await processBulkUploadTask(
810+
{ bulkUploadId: bulkUploadTask.id },
811+
getMockJobHelpers(),
812+
);
813+
814+
const updatedBulkUploadTask = await loadBulkUploadTask(
815+
db,
816+
testAuthContext,
817+
bulkUploadTask.id,
818+
);
819+
expect(updatedBulkUploadTask.status).toEqual(TaskStatus.COMPLETED);
820+
821+
const proposalBundle = await loadProposalBundle(
822+
db,
823+
testAuthContext,
824+
undefined,
825+
undefined,
826+
undefined,
827+
undefined,
828+
NO_LIMIT,
829+
NO_OFFSET,
830+
);
831+
const favoriteFileValues = proposalBundle.entries
832+
.flatMap((proposal) => proposal.versions)
833+
.flatMap((version) => version.fieldValues)
834+
.filter(
835+
(fv) => fv.applicationFormField.baseFieldShortCode === 'favorite_file',
836+
)
837+
.sort((a, b) => a.proposalVersionId - b.proposalVersionId);
838+
expect(favoriteFileValues).toEqual([
839+
expectObjectContaining({
840+
value: expectString(),
841+
isValid: true,
842+
file: expectObjectContaining({ name: 'one.txt' }),
843+
}),
844+
expectObjectContaining({
845+
value: '',
846+
isValid: false,
847+
file: null,
848+
}),
849+
expectObjectContaining({
850+
value: ' ',
851+
isValid: false,
852+
file: null,
853+
}),
854+
expectObjectContaining({
855+
value: ' one.txt ',
856+
isValid: true,
857+
file: expectObjectContaining({ name: 'one.txt' }),
858+
}),
859+
]);
860+
});
861+
751862
it('should create changemakers and changemaker-proposal relationships', async () => {
752863
const db = getDatabase();
753864
await createTestBaseFields(db);

src/tasks/processBulkUploadTask.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -632,14 +632,17 @@ export const processBulkUploadTask = async (
632632

633633
// File attachments are the one unique case where we need to convert the bulk upload value
634634
// into a PDC File ID. The value in the CSV is the relative path of the file within the
635-
// attachments archive.
635+
// attachments archive. A blank or whitespace-only cell means no attachment for that row.
636636
if (
637637
applicationFormField.baseField.dataType === BaseFieldDataType.FILE
638638
) {
639-
const attachmentFile =
640-
await attachmentsManager.getAttachmentFile(fieldValue);
641-
processedFieldValue = attachmentFile.id.toString();
642-
isValid = true;
639+
const trimmedPath = fieldValue.trim();
640+
if (trimmedPath !== '') {
641+
const attachmentFile =
642+
await attachmentsManager.getAttachmentFile(trimmedPath);
643+
processedFieldValue = attachmentFile.id.toString();
644+
isValid = true;
645+
}
643646
}
644647

645648
const proposalFieldValue = await createProposalFieldValue(

0 commit comments

Comments
 (0)