Skip to content

Commit 9493c35

Browse files
authored
Merge pull request #145 from multinet-app/csv-delimiters
Add a delimiter field to the csv upload for user specification
2 parents c73c643 + 392f71b commit 9493c35

File tree

4 files changed

+81
-2
lines changed

4 files changed

+81
-2
lines changed

multinet/api/tasks/upload/csv.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,25 @@ def process_row(row: Dict[str, Any], cols: Dict[str, TableTypeAnnotation.Type])
3838

3939
@shared_task(base=ProcessUploadTask)
4040
def process_csv(
41-
task_id: int, table_name: str, edge: bool, columns: Dict[str, TableTypeAnnotation.Type]
41+
task_id: int,
42+
table_name: str,
43+
edge: bool,
44+
columns: Dict[str, TableTypeAnnotation.Type],
45+
delimiter: str,
46+
quotechar: str,
4247
) -> None:
4348
upload: Upload = Upload.objects.get(id=task_id)
4449

4550
# Download data from S3/MinIO
4651
with upload.blob as blob_file:
4752
blob_file: BinaryIO = blob_file
48-
csv_rows = list(csv.DictReader(StringIO(blob_file.read().decode('utf-8'))))
53+
csv_rows = list(
54+
csv.DictReader(
55+
StringIO(blob_file.read().decode('utf-8')),
56+
delimiter=delimiter,
57+
quotechar=quotechar,
58+
)
59+
)
4960

5061
# Cast entries in each row to appropriate type, if necessary
5162
for i, row in enumerate(csv_rows):

multinet/api/tests/test_upload_csv.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ def airports_csv(
6161
'timezone': 'number',
6262
'year built': 'number',
6363
},
64+
'delimiter': ',',
65+
'quotechar': '\"',
6466
},
6567
format='json',
6668
)
@@ -106,6 +108,8 @@ def test_create_upload_model_invalid_columns(
106108
'edge': False,
107109
'table_name': 'table',
108110
'columns': {'foo': 'invalid'},
111+
'delimiter': ',',
112+
'quotechar': '\"',
109113
},
110114
format='json',
111115
)
@@ -114,6 +118,64 @@ def test_create_upload_model_invalid_columns(
114118
assert r.json() == {'columns': {'foo': ['"invalid" is not a valid choice.']}}
115119

116120

121+
@pytest.mark.django_db
122+
def test_create_upload_model_missing_delimiter(
123+
workspace: Workspace, user: User, authenticated_api_client
124+
):
125+
workspace.set_user_permission(user, WorkspaceRoleChoice.WRITER)
126+
r: Response = authenticated_api_client.post(
127+
f'/api/workspaces/{workspace.name}/uploads/csv/',
128+
{
129+
# Not an issue to specify invalid field_value, as that's checked after columns,
130+
# so the request will return before that is checked
131+
'field_value': 'field_value',
132+
'edge': False,
133+
'table_name': 'table',
134+
'columns': {
135+
'latitude': 'number',
136+
'longitude': 'number',
137+
'altitude': 'number',
138+
'timezone': 'number',
139+
'year built': 'number',
140+
},
141+
'quotechar': '\"',
142+
},
143+
format='json',
144+
)
145+
146+
assert r.status_code == 400
147+
assert r.json() == {'delimiter': ['This field is required.']}
148+
149+
150+
@pytest.mark.django_db
151+
def test_create_upload_model_missing_quotechar(
152+
workspace: Workspace, user: User, authenticated_api_client
153+
):
154+
workspace.set_user_permission(user, WorkspaceRoleChoice.WRITER)
155+
r: Response = authenticated_api_client.post(
156+
f'/api/workspaces/{workspace.name}/uploads/csv/',
157+
{
158+
# Not an issue to specify invalid field_value, as that's checked after columns,
159+
# so the request will return before that is checked
160+
'field_value': 'field_value',
161+
'edge': False,
162+
'table_name': 'table',
163+
'columns': {
164+
'latitude': 'number',
165+
'longitude': 'number',
166+
'altitude': 'number',
167+
'timezone': 'number',
168+
'year built': 'number',
169+
},
170+
'delimiter': ',',
171+
},
172+
format='json',
173+
)
174+
175+
assert r.status_code == 400
176+
assert r.json() == {'quotechar': ['This field is required.']}
177+
178+
117179
@pytest.mark.django_db
118180
@pytest.mark.parametrize('permission,status_code', [(None, 404), (WorkspaceRoleChoice.READER, 403)])
119181
def test_create_upload_model_csv_invalid_permissions(
@@ -164,6 +226,8 @@ def test_create_upload_model_invalid_field_value(
164226
'field_value': 'field_value',
165227
'edge': False,
166228
'table_name': 'table',
229+
'delimiter': ',',
230+
'quotechar': '\"',
167231
},
168232
format='json',
169233
)

multinet/api/views/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ class CSVUploadCreateSerializer(UploadCreateSerializer):
255255
child=serializers.ChoiceField(choices=TableTypeAnnotation.Type.choices),
256256
default=dict,
257257
)
258+
delimiter = serializers.CharField()
259+
quotechar = serializers.CharField()
258260

259261

260262
class D3JSONUploadCreateSerializer(UploadCreateSerializer):

multinet/api/views/upload.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ def upload_csv(self, request, parent_lookup_workspace__name: str):
9090
table_name=table_name,
9191
edge=serializer.validated_data['edge'],
9292
columns=serializer.validated_data['columns'],
93+
delimiter=serializer.validated_data['delimiter'],
94+
quotechar=serializer.validated_data['quotechar'],
9395
)
9496

9597
return Response(UploadReturnSerializer(upload).data, status=status.HTTP_200_OK)

0 commit comments

Comments
 (0)