Skip to content

Commit 87197e1

Browse files
authored
Merge pull request #2538 from IFRCGo/fix/bulk-upload-error-message
update error writer logic to write error details
2 parents 12db30c + b5e8ab0 commit 87197e1

File tree

3 files changed

+67
-11
lines changed

3 files changed

+67
-11
lines changed

local_units/bulk_upload.py

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from django.core.files.base import ContentFile
88
from django.db import transaction
9+
from rest_framework.exceptions import ErrorDetail
910
from rest_framework.serializers import Serializer
1011

1112
from local_units.models import HealthData, LocalUnit, LocalUnitBulkUpload, LocalUnitType
@@ -27,16 +28,69 @@ class BulkUploadError(Exception):
2728
class ErrorWriter:
2829
def __init__(self, fieldnames: list[str]):
2930
self._fieldnames = ["upload_status"] + fieldnames
31+
self._rows: list[dict[str, str]] = []
3032
self._output = io.StringIO()
3133
self._writer = csv.DictWriter(self._output, fieldnames=self._fieldnames)
3234
self._writer.writeheader()
35+
self._has_errors = False
36+
37+
def _format_errors(self, errors: dict) -> dict[str, list[str]]:
38+
"""Format serializer errors into field_name and list of messages."""
39+
error = {}
40+
for key, value in errors.items():
41+
if isinstance(value, dict):
42+
for inner_key, inner_value in self._format_errors(value).items():
43+
error[inner_key] = inner_value
44+
elif isinstance(value, list):
45+
error[key] = [self._clean_message(v) for v in value]
46+
else:
47+
error[key] = [self._clean_message(value)]
48+
return error
49+
50+
def _clean_message(self, msg: Any) -> str:
51+
"""Convert ErrorDetail or other objects into normal text."""
52+
if isinstance(msg, ErrorDetail):
53+
return str(msg)
54+
return str(msg)
55+
56+
def _update_csv_header_with_errors(self):
57+
"""Update the CSV with updated headers when new error columns are introduced."""
58+
self._output.seek(0)
59+
self._output.truncate()
60+
self._writer = csv.DictWriter(self._output, fieldnames=self._fieldnames)
61+
self._writer.writeheader()
62+
for row in self._rows:
63+
self._writer.writerow(row)
3364

3465
def write(
35-
self, row: dict[str, str], status: Literal[LocalUnitBulkUpload.Status.SUCCESS, LocalUnitBulkUpload.Status.FAILED]
66+
self,
67+
row: dict[str, str],
68+
status: Literal[LocalUnitBulkUpload.Status.SUCCESS, LocalUnitBulkUpload.Status.FAILED],
69+
error_detail: dict | None = None,
3670
) -> None:
37-
self._writer.writerow({"upload_status": status.name, **row})
38-
if status == LocalUnitBulkUpload.Status.FAILED:
39-
self._has_errors = True
71+
row_copy = {col: row.get(col, "") for col in self._fieldnames}
72+
row_copy["upload_status"] = status.name
73+
added_error_column = False
74+
75+
if status == LocalUnitBulkUpload.Status.FAILED and error_detail:
76+
formatted_errors = self._format_errors(error_detail)
77+
for field, messages in formatted_errors.items():
78+
error_col = f"{field}_error"
79+
80+
if error_col not in self._fieldnames:
81+
if field in self._fieldnames:
82+
col_idx = self._fieldnames.index(field)
83+
self._fieldnames.insert(col_idx + 1, error_col)
84+
else:
85+
self._fieldnames.append(error_col)
86+
87+
added_error_column = True
88+
row_copy[error_col] = "; ".join(messages)
89+
self._rows.append(row_copy)
90+
if added_error_column:
91+
self._update_csv_header_with_errors()
92+
else:
93+
self._writer.writerow(row_copy)
4094

4195
def to_content_file(self) -> ContentFile:
4296
return ContentFile(self._output.getvalue().encode("utf-8"))
@@ -70,17 +124,17 @@ def process_row(self, data: Dict[str, Any]) -> bool:
70124
if serializer.is_valid():
71125
self.bulk_manager.add(LocalUnit(**serializer.validated_data))
72126
return True
127+
self.error_detail = serializer.errors
73128
return False
74129

75130
def run(self) -> None:
76131
with self.bulk_upload.file.open("rb") as csv_file:
77132
file = io.TextIOWrapper(csv_file, encoding="utf-8")
78133
csv_reader = csv.DictReader(file)
79134
fieldnames = csv_reader.fieldnames or []
80-
81135
try:
82136
self._validate_csv(fieldnames)
83-
except ValueError as e:
137+
except BulkUploadError as e:
84138
self.bulk_upload.status = LocalUnitBulkUpload.Status.FAILED
85139
self.bulk_upload.error_message = str(e)
86140
self.bulk_upload.save(update_fields=["status", "error_message"])
@@ -99,7 +153,9 @@ def run(self) -> None:
99153
self.error_writer.write(row_data, status=LocalUnitBulkUpload.Status.SUCCESS)
100154
else:
101155
self.failed_count += 1
102-
self.error_writer.write(row_data, status=LocalUnitBulkUpload.Status.FAILED)
156+
self.error_writer.write(
157+
row_data, status=LocalUnitBulkUpload.Status.FAILED, error_detail=self.error_detail
158+
)
103159
logger.warning(f"[BulkUpload:{self.bulk_upload.pk}] Row '{row_index}' failed")
104160

105161
if self.failed_count > 0:
@@ -175,7 +231,7 @@ def _validate_csv(self, fieldnames) -> None:
175231
raise ValueError("Invalid local unit type")
176232

177233
if present_health_fields and local_unit_type.name.strip().lower() != "health care":
178-
raise ValueError(f"Health data are not allowed for this type: {local_unit_type.name}.")
234+
raise BulkUploadError(f"Health data are not allowed for this type: {local_unit_type.name}.")
179235

180236

181237
class BulkUploadHealthData(BaseBulkUpload[LocalUnitUploadContext]):

local_units/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,7 @@ def validate_level(self, value):
905905
level_name = value.strip().lower()
906906
level_id = self.level_map.get(level_name)
907907
if not level_id:
908-
raise serializers.ValidationError({"Level": gettext("Level '{value}' is not valid")})
908+
raise serializers.ValidationError(gettext("Level is not valid"))
909909
return level_id
910910

911911
def validate(self, validated_data):

local_units/tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def process_bulk_upload_local_unit(bulk_upload_id: int) -> None:
138138
bulk_upload: LocalUnitBulkUpload | None = LocalUnitBulkUpload.objects.filter(id=bulk_upload_id).first()
139139

140140
if not bulk_upload:
141-
logger.error(f"BulkUploadLocalUnit:'{bulk_upload_id}' Not found.", exc_info=True)
141+
logger.warning(f"BulkUploadLocalUnit:'{bulk_upload_id}' Not found.", exc_info=True)
142142
return
143143
try:
144144
if (
@@ -149,6 +149,6 @@ def process_bulk_upload_local_unit(bulk_upload_id: int) -> None:
149149
BaseBulkUploadLocalUnit(bulk_upload).run()
150150

151151
except Exception as exc:
152-
logger.error(f"BulkUploadLocalUnit:'{bulk_upload_id}' Failed with exception: {exc}", exc_info=True)
152+
logger.warning(f"BulkUploadLocalUnit:'{bulk_upload_id}' Failed with exception: {exc}", exc_info=True)
153153
bulk_upload.update_status(LocalUnitBulkUpload.Status.FAILED)
154154
raise exc

0 commit comments

Comments
 (0)