Skip to content

Commit c04c512

Browse files
committed
feat(bulk-upload): add .xlsm file support to bulk upload
1 parent d504130 commit c04c512

File tree

6 files changed

+66
-63
lines changed

6 files changed

+66
-63
lines changed
Binary file not shown.

local_units/bulk_upload.py

Lines changed: 56 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def to_content_file(self) -> ContentFile:
115115

116116
class BaseBulkUpload(Generic[ContextType]):
117117
serializer_class: Type[Serializer]
118+
HEADER_MAP: dict[str, str]
118119

119120
def __init__(self, bulk_upload: LocalUnitBulkUpload):
120121
if self.serializer_class is None:
@@ -152,70 +153,71 @@ def process_row(self, data: Dict[str, Any]) -> bool:
152153
return False
153154

154155
def run(self) -> None:
155-
with self.bulk_upload.file.open("rb") as f:
156-
try:
157-
# TODO(sudip): Use read_only while reading xlsx file
158-
workbook = openpyxl.load_workbook(f, data_only=True)
156+
header_row_index = 2
157+
data_row_index = header_row_index + 2
158+
159+
try:
160+
with self.bulk_upload.file.open("rb") as f:
161+
workbook = openpyxl.load_workbook(f, data_only=True, read_only=True)
159162
sheet = workbook.active
160-
header_row_index = 2
161-
data_row_index = header_row_index + 2
162163

163-
# Read header row
164164
headers = next(sheet.iter_rows(values_only=True, min_row=header_row_index, max_row=header_row_index))
165165
raw_fieldnames = [str(h).strip() for h in headers if h and str(h).strip()]
166-
header_map = getattr(self, "HEADER_MAP", {}) or {}
166+
header_map = self.HEADER_MAP or {}
167167
mapped_fieldnames = [header_map.get(h, h) for h in raw_fieldnames]
168168
fieldnames = mapped_fieldnames
169169

170170
if self.is_excel_data_empty(sheet, data_start_row=data_row_index):
171-
raise BulkUploadError("The uploaded Excel file is empty. Please provide at least one data row.")
171+
raise BulkUploadError("The uploaded file is empty. Please provide at least one data row.")
172172

173173
self._validate_type(fieldnames)
174-
data_rows = (
175-
row
176-
for row in sheet.iter_rows(values_only=True, min_row=4)
177-
if any(cell is not None for cell in row) # skip the empty rows
178-
)
179-
except Exception as e:
180-
self.bulk_upload.status = LocalUnitBulkUpload.Status.FAILED
181-
self.bulk_upload.error_message = str(e)
182-
self.bulk_upload.save(update_fields=["status", "error_message"])
183-
logger.warning(f"[BulkUpload:{self.bulk_upload.pk}] Validation error: {str(e)}")
184-
return
185-
186-
context = self.get_context().__dict__
187-
self.error_writer = ErrorWriter(fieldnames=raw_fieldnames, header_map=header_map)
188-
189-
try:
190-
with transaction.atomic():
191-
self.delete_existing_local_unit()
192-
193-
for row_index, row_values in enumerate(data_rows, start=data_row_index):
194-
row_dict = dict(zip(fieldnames, row_values))
195-
row_dict = {**row_dict, **context}
196-
# Convert datetime objects to strings
197-
for key, value in row_dict.items():
198-
if isinstance(value, (datetime, date)):
199-
row_dict[key] = value.strftime("%Y-%m-%d")
200-
if self.process_row(row_dict):
201-
self.success_count += 1
202-
self.error_writer.write(row_dict, status=LocalUnitBulkUpload.Status.SUCCESS)
203-
else:
204-
self.failed_count += 1
205-
self.error_writer.write(
206-
row_dict,
207-
status=LocalUnitBulkUpload.Status.FAILED,
208-
error_detail=self.error_detail,
209-
)
210-
logger.warning(f"[BulkUpload:{self.bulk_upload.pk}] Row {row_index} failed")
211-
212-
if self.failed_count > 0:
213-
raise BulkUploadError("Bulk upload failed with some errors.")
214-
215-
self.bulk_manager.done()
216-
self._finalize_success()
217-
218-
except BulkUploadError:
174+
self.error_writer = ErrorWriter(fieldnames=raw_fieldnames, header_map=header_map)
175+
context = self.get_context().__dict__
176+
177+
with transaction.atomic():
178+
self.delete_existing_local_unit()
179+
180+
for row_index, row_values in enumerate(
181+
sheet.iter_rows(values_only=True, min_row=data_row_index),
182+
start=data_row_index,
183+
):
184+
if not any(cell is not None for cell in row_values):
185+
continue # skip empty rows
186+
187+
row_dict = dict(zip(fieldnames, row_values))
188+
row_dict = {**row_dict, **context}
189+
190+
# Convert date/datetime to str
191+
for key, value in row_dict.items():
192+
if isinstance(value, (datetime, date)):
193+
row_dict[key] = value.strftime("%Y-%m-%d")
194+
195+
if self.process_row(row_dict):
196+
self.success_count += 1
197+
self.error_writer.write(row_dict, status=LocalUnitBulkUpload.Status.SUCCESS)
198+
else:
199+
self.failed_count += 1
200+
self.error_writer.write(
201+
row_dict,
202+
status=LocalUnitBulkUpload.Status.FAILED,
203+
error_detail=self.error_detail,
204+
)
205+
logger.warning(f"[BulkUpload:{self.bulk_upload.pk}] Row {row_index} failed")
206+
207+
if self.failed_count > 0:
208+
raise BulkUploadError()
209+
210+
self.bulk_manager.done()
211+
self._finalize_success()
212+
213+
workbook.close()
214+
215+
except Exception as e:
216+
self.bulk_upload.status = LocalUnitBulkUpload.Status.FAILED
217+
self.bulk_upload.error_message = str(e)
218+
self.bulk_upload.save(update_fields=["status", "error_message"])
219+
if isinstance(e, BulkUploadError):
220+
logger.warning(f"[BulkUpload:{self.bulk_upload.pk}] error: {e}")
219221
self._finalize_failure()
220222

221223
def _finalize_success(self) -> None:

local_units/serializers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,7 @@ def update(self, instance, validated_data):
673673

674674

675675
class LocalUnitBulkUploadSerializer(serializers.ModelSerializer):
676+
VALID_FILE_EXTENSIONS = (".xlsx", ".xlsm")
676677
country = serializers.PrimaryKeyRelatedField(
677678
queryset=Country.objects.filter(
678679
is_deprecated=False, independent=True, iso3__isnull=False, record_type=CountryType.COUNTRY
@@ -703,8 +704,8 @@ class Meta:
703704
)
704705

705706
def validate_file(self, file):
706-
if not file.name.endswith(".xlsx"):
707-
raise serializers.ValidationError(gettext("File must be a xlsx file."))
707+
if not file.name.lower().endswith(self.VALID_FILE_EXTENSIONS):
708+
raise serializers.ValidationError(gettext("The uploaded file must be an Excel document (.xlsx or .xlsm)."))
708709
if file.size > 10 * 1024 * 1024:
709710
raise serializers.ValidationError(gettext("File must be less than 10 MB."))
710711
return file

local_units/test_views.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,12 +1535,12 @@ def setUpTestData(cls):
15351535
cls.professional_training_facilities = ProfessionalTrainingFacility.objects.create(code=1, name="Nurses")
15361536
cls.general_medical_services = GeneralMedicalService.objects.create(code=1, name="Minor Trauma")
15371537

1538-
file_path = os.path.join(settings.TEST_DIR, "local_unit/test-health.xlsx")
1538+
file_path = os.path.join(settings.TEST_DIR, "local_unit/test-health.xlsm")
15391539
with open(file_path, "rb") as f:
15401540
cls._file_content = f.read()
15411541

1542-
def create_upload_file(cls, filename="test-health.xlsx"):
1543-
return SimpleUploadedFile(filename, cls._file_content, content_type="text/xlsx")
1542+
def create_upload_file(cls, filename="test-health.xlsm"):
1543+
return SimpleUploadedFile(filename, cls._file_content, content_type="text/xlsm")
15441544

15451545
def test_bulk_upload_health_with_incorrect_country(cls):
15461546
"""
@@ -1565,7 +1565,7 @@ def test_bulk_upload_health_with_incorrect_country(cls):
15651565

15661566
def test_bulk_upload_health_fails_and_does_not_delete(cls):
15671567
"""
1568-
Should fail and keep existing LocalUnits & HealthData when CSV invalid.
1568+
Should fail and keep existing LocalUnits & HealthData when file invalid.
15691569
"""
15701570
health_data = HealthDataFactory.create_batch(
15711571
5,
@@ -1644,12 +1644,12 @@ def test_empty_health_template_file(cls):
16441644
"""
16451645

16461646
file_path = os.path.join(
1647-
settings.STATICFILES_DIRS[0], "files", "local_units", "Health Care Bulk Import Template - Local Units.xlsx"
1647+
settings.STATICFILES_DIRS[0], "files", "local_units", "Health Care Bulk Import Template - Local Units.xlsm"
16481648
)
16491649
with open(file_path, "rb") as f:
16501650
file_content = f.read()
16511651
empty_file = SimpleUploadedFile(
1652-
name="Health Care Bulk Import Template - Local Units.xlsx", content=file_content, content_type="text/xlsx"
1652+
name="Health Care Bulk Import Template - Local Units.xlsm", content=file_content, content_type="text/xlsm"
16531653
)
16541654
health_data = HealthDataFactory.create_batch(
16551655
5,

local_units/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ class LocalUnitBulkUploadViewSet(
436436
def get_bulk_upload_template(self, request):
437437
template_type = request.query_params.get("bulk_upload_template", "local_unit")
438438
if template_type == "health_care":
439-
file_url = request.build_absolute_uri(static("files/local_units/Health Care Bulk Import Template - Local Units.xlsx"))
439+
file_url = request.build_absolute_uri(static("files/local_units/Health Care Bulk Import Template - Local Units.xlsm"))
440440
else:
441441
file_url = request.build_absolute_uri(
442442
static("files/local_units/Administrative Bulk Import Template - Local Units.xlsx")
-141 KB
Binary file not shown.

0 commit comments

Comments
 (0)