Skip to content

Commit 73eaeea

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 73eaeea

4 files changed

Lines changed: 112 additions & 2 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
proposal_submitter_email,organization_name,favorite_file
2+
foo@example.com,Foo LLC.,one.txt
3+
foo@example.com,Bar Inc.,

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

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

751+
it('should process rows with blank 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+
]);
850+
});
851+
751852
it('should create changemakers and changemaker-proposal relationships', async () => {
752853
const db = getDatabase();
753854
await createTestBaseFields(db);

src/tasks/processBulkUploadTask.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,9 +632,11 @@ 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 cell means no attachment for that row.
636636
if (
637-
applicationFormField.baseField.dataType === BaseFieldDataType.FILE
637+
applicationFormField.baseField.dataType ===
638+
BaseFieldDataType.FILE &&
639+
fieldValue !== ''
638640
) {
639641
const attachmentFile =
640642
await attachmentsManager.getAttachmentFile(fieldValue);

0 commit comments

Comments
 (0)