66
77from django .core .files .base import ContentFile
88from django .db import transaction
9+ from rest_framework .exceptions import ErrorDetail
910from rest_framework .serializers import Serializer
1011
1112from local_units .models import HealthData , LocalUnit , LocalUnitBulkUpload , LocalUnitType
@@ -27,16 +28,69 @@ class BulkUploadError(Exception):
2728class 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
181237class BulkUploadHealthData (BaseBulkUpload [LocalUnitUploadContext ]):
0 commit comments