Skip to content

Commit 227bb06

Browse files
patsatsiapatsatsiadomdinicola
authored
Feature/change export logic (#272)
* Add two new columns is_valid and errors to export excel file * Alter 'export_bulk_update_template' function Add annotated column is_valid * Minor logic improvements Move qs generation to a dedicated function * Add tests Add a new test case 'test_create_bulk_update_template_with_errors' * Add a new field "include_errors" to "BulkUpdateExportForm" * Add columns "is_valid" and "errors" conditionally on include_errors flag * Alter test cases --------- Co-authored-by: patsatsia <giorgi.patsatsia@valor-software.com> Co-authored-by: Domenico <ddinicola@unicef.org>
1 parent 643f295 commit 227bb06

File tree

5 files changed

+84
-5
lines changed

5 files changed

+84
-5
lines changed

src/country_workspace/workspaces/admin/cleaners/actions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from country_workspace.state import state
1313
from country_workspace.utils.fields import rdi_name_default
1414
from country_workspace.workspaces.admin.forms import BulkUpdateExportForm
15-
1615
from .bulk_update import export_bulk_update_template
1716
from .calculate_checksum import calculate_checksum_impl
1817
from .mass_update import MassUpdateForm, mass_update_impl
@@ -143,6 +142,8 @@ def bulk_update_export(
143142
ctx["form"] = form
144143
if "_export" in request.POST and form.is_valid():
145144
columns = ["id", "version"] + form.cleaned_data["fields"]
145+
if form.cleaned_data.get("include_errors", False):
146+
columns += ["is_valid", "errors"]
146147
opts = queryset.model._meta
147148
job = AsyncJob.objects.create(
148149
description=bulk_update_export.short_description,

src/country_workspace/workspaces/admin/cleaners/bulk_update.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime
44
from io import BytesIO
55
from typing import TYPE_CHECKING, Any
6+
from django.db.models import Case, When, Value, Q, CharField
67

78
from constance import config as constance_config
89
from django import forms
@@ -20,13 +21,13 @@
2021
from xlsxwriter.format import Format
2122
from xlsxwriter.worksheet import Worksheet
2223
from country_workspace.models import AsyncJob, Program, Country
24+
from country_workspace.models.base import Validable
2325
from country_workspace.state import state
2426
from country_workspace.storages import MEDIA_STORAGE
2527
from country_workspace.workspaces.admin.cleaners.exceptions import BulkImportError, BulkImportFileProcessingError
28+
from django.db.models import QuerySet
2629

2730
if TYPE_CHECKING:
28-
from django.db.models import QuerySet
29-
3031
from country_workspace.types import Beneficiary
3132

3233
"""
@@ -322,7 +323,7 @@ def _send_template_email(job: AsyncJob, out: BytesIO, filename: str) -> None:
322323
def export_bulk_update_template(job: AsyncJob) -> str:
323324
with state.set(tenant=job.program.country_office, program=job.program):
324325
model = apps.get_model(job.config["model_name"])
325-
queryset = model.objects.filter(pk__in=job.config["pks"])
326+
queryset = _get_queryset_with_is_valid_annotation(model, job)
326327

327328
out = create_bulk_update_template(queryset, job.program, job.config["columns"])
328329
filename = f"bulk_update_template/{job.program.pk}/{job.owner.pk}/{job.config['model_name']}.xlsx"
@@ -336,6 +337,21 @@ def export_bulk_update_template(job: AsyncJob) -> str:
336337
return filepath
337338

338339

340+
def _get_queryset_with_is_valid_annotation(model: Validable, job: AsyncJob) -> QuerySet:
341+
qs = model.objects.filter(pk__in=job.config["pks"])
342+
if (columns := job.config.get("columns")) and ("is_valid" in columns and "errors" in columns):
343+
qs = qs.annotate(
344+
is_valid=Case(
345+
When(last_checked__isnull=True, then=Value("Not Checked")),
346+
When(Q(last_checked__isnull=False) & Q(errors={}), then=Value("True")),
347+
default=Value("False"),
348+
output_field=CharField(),
349+
)
350+
)
351+
352+
return qs
353+
354+
339355
def import_bulk_update_file(job: AsyncJob, entity_getter: Callable[[int], Any]) -> dict[str, Any]: # noqa: C901
340356
total = {"processed": 0, "not_found": [], "errors": {}}
341357
version_check_enabled = constance_config.CONCURRENCY_GUARD

src/country_workspace/workspaces/admin/forms.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414

1515
class BulkUpdateExportForm(BaseActionForm):
1616
fields = forms.MultipleChoiceField(choices=[], widget=forms.CheckboxSelectMultiple())
17+
include_errors = forms.BooleanField(
18+
label=_("Include errors and validity status"),
19+
required=False,
20+
initial=False,
21+
)
1722

1823
def __init__(self, *args: Any, **kwargs: Any) -> None:
1924
checker: "DataChecker" = kwargs.pop("checker")

tests/workspace/actions/stub.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"role",
1414
],
1515
}
16+
header_last: Final[tuple[str, ...]] = ["is_valid", "errors"]
1617

1718
batch_name: Final[str] = "TestBatch"
1819
batch_size: Final[int] = 10

tests/workspace/actions/test_ws_bulk.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1+
import datetime
12
import io
23
from typing import TYPE_CHECKING, Any
34
from unittest import mock
5+
from unittest.mock import Mock
46

57
import openpyxl
68
import pytest
79
import xlsxwriter
810
from django.urls import reverse
11+
from django.utils import timezone
912
from testutils.factories import FlexFieldFactory
1013
from testutils.utils import select_office
1114
from webtest import Upload
1215

1316
from country_workspace.state import state
14-
from country_workspace.workspaces.admin.cleaners.bulk_update import TYPES, create_bulk_update_template
17+
from country_workspace.workspaces.admin.cleaners.bulk_update import (
18+
TYPES,
19+
create_bulk_update_template,
20+
_get_queryset_with_is_valid_annotation,
21+
)
22+
from tests.extras.testutils.factories import CountryHouseholdFactory
1523
from tests.workspace.actions import stub
1624

1725
if TYPE_CHECKING:
@@ -138,6 +146,54 @@ def test_create_bulk_update_template(household: "CountryHousehold", force_migrat
138146
assert headers == selected_fields
139147

140148

149+
def test_create_bulk_update_template_with_errors(program):
150+
CountryHouseholdFactory.create_batch(2, batch__program=program)
151+
CountryHouseholdFactory.create_batch(
152+
3, batch__program=program, last_checked=(timezone.now() - datetime.timedelta(days=3)), errors={}
153+
)
154+
CountryHouseholdFactory.create_batch(
155+
4,
156+
batch__program=program,
157+
last_checked=(timezone.now() - datetime.timedelta(days=4)),
158+
errors={"cool_error": "cooler error text"},
159+
)
160+
selected_fields = stub.header_base + stub.header_add["ind"] + stub.header_last
161+
162+
job = Mock()
163+
job.config = {
164+
"pks": CountryHouseholdFactory._meta.model.objects.values_list("id", flat=True),
165+
"columns": selected_fields,
166+
}
167+
qs = _get_queryset_with_is_valid_annotation(CountryHouseholdFactory._meta.model, job)
168+
169+
ret = create_bulk_update_template(
170+
qs,
171+
program,
172+
selected_fields,
173+
)
174+
175+
workbook = openpyxl.load_workbook(io.BytesIO(ret.getvalue()))
176+
sheet = workbook.worksheets[0]
177+
headers = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1))]
178+
assert headers == selected_fields
179+
180+
valid_count = 0
181+
invalid_count = 0
182+
not_checked_count = 0
183+
for row in sheet.iter_rows(min_row=2):
184+
is_valid = row[-2].value
185+
if is_valid == "True":
186+
valid_count += 1
187+
elif is_valid == "False":
188+
invalid_count += 1
189+
elif is_valid == "Not Checked":
190+
not_checked_count += 1
191+
192+
assert valid_count == 3
193+
assert invalid_count == 4
194+
assert not_checked_count == 2
195+
196+
141197
def test_bulk_update_export(
142198
app: "DjangoTestApp",
143199
force_migrated_records,

0 commit comments

Comments
 (0)