6
6
7
7
from django .core .files .base import ContentFile
8
8
from django .db import transaction
9
+ from rest_framework .exceptions import ErrorDetail
9
10
from rest_framework .serializers import Serializer
10
11
11
12
from local_units .models import HealthData , LocalUnit , LocalUnitBulkUpload , LocalUnitType
@@ -27,16 +28,69 @@ class BulkUploadError(Exception):
27
28
class ErrorWriter :
28
29
def __init__ (self , fieldnames : list [str ]):
29
30
self ._fieldnames = ["upload_status" ] + fieldnames
31
+ self ._rows : list [dict [str , str ]] = []
30
32
self ._output = io .StringIO ()
31
33
self ._writer = csv .DictWriter (self ._output , fieldnames = self ._fieldnames )
32
34
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 )
33
64
34
65
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 ,
36
70
) -> 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 )
40
94
41
95
def to_content_file (self ) -> ContentFile :
42
96
return ContentFile (self ._output .getvalue ().encode ("utf-8" ))
@@ -70,17 +124,17 @@ def process_row(self, data: Dict[str, Any]) -> bool:
70
124
if serializer .is_valid ():
71
125
self .bulk_manager .add (LocalUnit (** serializer .validated_data ))
72
126
return True
127
+ self .error_detail = serializer .errors
73
128
return False
74
129
75
130
def run (self ) -> None :
76
131
with self .bulk_upload .file .open ("rb" ) as csv_file :
77
132
file = io .TextIOWrapper (csv_file , encoding = "utf-8" )
78
133
csv_reader = csv .DictReader (file )
79
134
fieldnames = csv_reader .fieldnames or []
80
-
81
135
try :
82
136
self ._validate_csv (fieldnames )
83
- except ValueError as e :
137
+ except BulkUploadError as e :
84
138
self .bulk_upload .status = LocalUnitBulkUpload .Status .FAILED
85
139
self .bulk_upload .error_message = str (e )
86
140
self .bulk_upload .save (update_fields = ["status" , "error_message" ])
@@ -99,7 +153,9 @@ def run(self) -> None:
99
153
self .error_writer .write (row_data , status = LocalUnitBulkUpload .Status .SUCCESS )
100
154
else :
101
155
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
+ )
103
159
logger .warning (f"[BulkUpload:{ self .bulk_upload .pk } ] Row '{ row_index } ' failed" )
104
160
105
161
if self .failed_count > 0 :
@@ -175,7 +231,7 @@ def _validate_csv(self, fieldnames) -> None:
175
231
raise ValueError ("Invalid local unit type" )
176
232
177
233
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 } ." )
179
235
180
236
181
237
class BulkUploadHealthData (BaseBulkUpload [LocalUnitUploadContext ]):
0 commit comments