Skip to content

Commit 81d2e9b

Browse files
chg ! rebase
1 parent fd2034f commit 81d2e9b

File tree

6 files changed

+112
-66
lines changed

6 files changed

+112
-66
lines changed

src/country_workspace/contrib/aurora/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(self, *args: tuple, program: Program | None = None, **kwargs: dict)
3434
self.program = program
3535
if program:
3636
self.fields["registration"].queryset = Registration.objects.filter(project__program=program, active=True)
37-
if not getattr(program.beneficiary_group, "master_detail", False):
37+
if not (program.beneficiary_group and program.beneficiary_group.master_detail):
3838
self.fields = {
3939
key: value
4040
for key, value in self.fields.items()

src/country_workspace/contrib/aurora/pipeline.py

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
from country_workspace.contrib.aurora.client import AuroraClient
66
from country_workspace.models import AsyncJob, Batch, Household, Individual
7-
from country_workspace.utils.config import BatchNameConfig, FailIfAlienConfig
8-
from country_workspace.utils.fields import uppercase_field_value, clean_field_names
7+
from country_workspace.utils.config import BatchNameConfig
8+
from country_workspace.utils.fields import clean_field_names
99

1010

11-
class Config(BatchNameConfig, FailIfAlienConfig):
11+
class Config(BatchNameConfig):
1212
registration_reference_pk: str | None
1313
master_detail: bool
1414
household_column_prefix: NotRequired[str]
@@ -70,12 +70,11 @@ def create_household(batch: Batch, data: dict[str, Any], prefix: str) -> Househo
7070
ValueError: If multiple household entries are found in the provided data.
7171
7272
"""
73-
flex_fields = _collect_by_prefix(data, prefix)
74-
if len(flex_fields) > 1:
73+
hh_data = _collect_by_prefix(data, prefix)
74+
if len(hh_data) > 1:
7575
raise ValueError("Multiple households found")
76-
flex_fields = next(iter(flex_fields.values()), {})
77-
return batch.program.households.create(batch=batch, flex_fields=clean_field_names(flex_fields))
78-
# return batch.program.households.create(batch=batch, flex_fields=flex_fields)
76+
flex_fields = clean_field_names(next(iter(hh_data.values()), {}))
77+
return batch.program.households.create(batch=batch, flex_fields=flex_fields)
7978

8079

8180
def create_individuals(
@@ -97,24 +96,21 @@ def create_individuals(
9796
household, individuals = None, []
9897
head_found = False
9998

100-
# for raw_individual in data.values():
101-
# individual = clean_field_names(raw_individual)
102-
# if not head_found:
10399
inds_data = _collect_by_prefix(data, cfg.get("individuals_column_prefix"))
104100

105101
if inds_data and cfg["master_detail"] and (hh_prefix := cfg.get("household_column_prefix")):
106102
household = create_household(batch, data, hh_prefix)
107103

108-
for individual in inds_data.values():
109-
household_label_column = cfg.get("household_label_column")
110-
if household and household_label_column and not head_found:
111-
head_found = _update_household_label_from_individual(household, individual, household_label_column)
104+
for ind_data in inds_data.values():
105+
flex_fields = clean_field_names(ind_data)
106+
if household and (hh_label := cfg.get("household_label_column")) and not head_found:
107+
head_found = _update_household_label_from_individual(household, flex_fields, hh_label)
112108
individuals.append(
113109
Individual(
114110
batch=batch,
115111
household_id=household.pk if household else None,
116-
name=individual.get("given_name", ""),
117-
flex_fields=individual,
112+
name=flex_fields.get("given_name", ""),
113+
flex_fields=flex_fields,
118114
)
119115
)
120116
return batch.program.individuals.bulk_create(individuals, batch_size=1000)
@@ -133,6 +129,9 @@ def _collect_by_prefix(data: dict[str, Any], prefix: str) -> dict[str, dict[str,
133129
and, for specific fields, values converted to uppercase. Returns an empty dictionary if no
134130
matching keys are found.
135131
132+
Raises:
133+
ValueError: If a key with the specified prefix does not contain an underscore after the prefix.
134+
136135
Examples:
137136
>>> data = {"user_0_relationship": "head", "user_0_gender": "male", "user_1_gender": "female"}
138137
>>> _collect_by_prefix(data, "user_")
@@ -144,25 +143,22 @@ def _collect_by_prefix(data: dict[str, Any], prefix: str) -> dict[str, dict[str,
144143
result = {}
145144
for k, v in data.items():
146145
if (stripped := k.removeprefix(prefix)) != k:
147-
index, field = stripped.split("_", 1)
148-
result.setdefault(index, {})[field] = uppercase_field_value(field, v)
149-
# for key, value in data.items():
150-
# if not key.startswith(prefix):
151-
# continue
152-
# index, field = key.removeprefix(prefix).split("_", 1)
153-
# clean_field = clean_field_name(field)
154-
# result.setdefault(index, {})[clean_field] = uppercase_field_value(clean_field, value)
155-
# return result
146+
try:
147+
index, field = stripped.split("_", 1)
148+
result.setdefault(index, {})[field] = v
149+
except ValueError:
150+
raise ValueError(f"Field name '{k}' after removing prefix '{prefix}' must contain an underscore.")
151+
return result
156152

157153

158154
def _update_household_label_from_individual(
159-
household: Household, individual: Mapping[str, Any], household_label_column: str
155+
household: Household, ind_data: Mapping[str, Any], household_label_column: str
160156
) -> bool:
161157
"""Update the household's name based on an individual's role and specified name field.
162158
163159
Args:
164160
household (Household): The household instance to update.
165-
individual (dict[str, Any]): A dictionary containing the individual's data,
161+
ind_data (dict[str, Any]): A dictionary containing the individual's data,
166162
including relationship status and potential household name.
167163
household_label_column (str): The key in the individual's data that stores
168164
the name to assign to the household.
@@ -171,8 +167,8 @@ def _update_household_label_from_individual(
171167
bool: True if the household name was updated (individual is head and name provided), False otherwise.
172168
173169
"""
174-
is_head = any(individual.get(k) == RELATIONSHIP_HEAD for k in individual if k.startswith(RELATIONSHIP_FIELDNAME))
175-
name = individual.get(household_label_column)
170+
is_head = any(ind_data.get(k) == RELATIONSHIP_HEAD for k in ind_data if k == RELATIONSHIP_FIELDNAME)
171+
name = ind_data.get(household_label_column)
176172
if is_head and name:
177173
household.name = name
178174
household.save(update_fields=["name"])

src/country_workspace/utils/fields.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212

1313
TO_REMOVE = "_h_c", "_h_f", "_i_c", "_i_f"
14+
TO_UPPERCASE = "relationship", "gender", "disability", "residence_status"
1415

1516

1617
def clean_field_name(v: str) -> str:
@@ -27,12 +28,21 @@ def clean_field_name(v: str) -> str:
2728

2829

2930
def clean_field_names(record: Record) -> Record:
30-
return {clean_field_name(k): v for k, v in record.items()}
31+
"""Clean all field names in a record by normalizing them.
32+
33+
Args:
34+
record (dict): A dictionary with field names as keys and their values.
35+
36+
Returns:
37+
dict: A new dictionary with cleaned field names and original values.
38+
39+
"""
40+
return {clean_field_name(k): uppercase_field_value(k, v) for k, v in record.items()}
3141

3242

3343
def uppercase_field_value(k: str, v: Any) -> str:
3444
"""
35-
Convert the given field value to uppercase if applicable.
45+
Convert the given field value to uppercase if its name starts with specific prefixes.
3646
3747
Args:
3848
k (str): The name of the field.
@@ -42,5 +52,4 @@ def uppercase_field_value(k: str, v: Any) -> str:
4252
str: The uppercase value if applicable or the original value.
4353
4454
"""
45-
to_uppercase = ("relationship", "gender", "disability", "residence_status")
46-
return v.upper() if isinstance(v, str) and k in to_uppercase else v
55+
return v.upper() if isinstance(v, str) and any(k.startswith(prefix) for prefix in TO_UPPERCASE) else v

src/country_workspace/workspaces/admin/program.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,9 @@ def import_aurora(self, request: HttpRequest, program: "CountryProgram") -> "Imp
311311
"batch_name": form.cleaned_data["batch_name"] or batch_name_default(),
312312
"registration_reference_pk": getattr(form.cleaned_data.get("registration"), "reference_pk", None),
313313
"individuals_column_prefix": form.cleaned_data["individuals_column_prefix"],
314-
"fail_if_alien": form.cleaned_data["fail_if_alien"],
315-
"master_detail": (master_detail := getattr(program.beneficiary_group, "master_detail", False)),
314+
"master_detail": (
315+
master_detail := (program.beneficiary_group.master_detail if program.beneficiary_group else False)
316+
),
316317
**(
317318
{
318319
"household_column_prefix": form.cleaned_data.get("household_column_prefix"),

tests/contrib/aurora/stub.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,16 @@
131131
},
132132
],
133133
},
134+
"invalid_key": {
135+
"page": 1,
136+
"results": [
137+
{
138+
"id": 9,
139+
"flatten": {
140+
"individuals_wrong": "value",
141+
"household_invalid": "data",
142+
},
143+
},
144+
],
145+
},
134146
}

tests/workspace/test_ws_import.py

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from constance import config
77
from django.urls import reverse
88
from pathlib import Path
9-
from webtest import Upload
9+
from webtest import forms, Upload
10+
1011

1112
from country_workspace.state import state
1213
from tests.contrib.aurora import stub
@@ -81,28 +82,10 @@ def test_import_data_rdi(force_migrated_records, app, program):
8182
assert head.name == "Edward Jeffrey Rogers"
8283

8384

84-
@pytest.mark.django_db(transaction=True)
85-
@pytest.mark.parametrize(
86-
("stub_data", "error_expected", "hh_count", "ind_count", "error_message"),
87-
[
88-
(stub.imported["correct"], False, 2, 3, None), # 2 hh: 1st with 1 ind, 2nd with 2 inds
89-
(stub.imported["no_individuals"], False, 0, 0, None), # No individuals
90-
(stub.imported["multiple_households"], True, 0, 1, "Multiple households found"), # Multiple households error
91-
(stub.imported["empty_household_data"], False, 1, 1, None), # Only ind without hh data
92-
(stub.imported["update_head_name"], False, 1, 1, None), # Household name updated from head
93-
],
94-
)
95-
def test_import_data_aurora(
96-
force_migrated_records: None,
97-
app: "DjangoTestApp",
98-
program: "CountryProgram",
99-
mocked_responses: responses.RequestsMock,
100-
stub_data: dict[str, Any],
101-
error_expected: bool,
102-
hh_count: int,
103-
ind_count: int,
104-
error_message: str,
105-
) -> None:
85+
@pytest.fixture
86+
def form_aurora(
87+
app: "DjangoTestApp", program: "CountryProgram", mocked_responses: responses.RequestsMock, stub_data: dict[str, Any]
88+
) -> forms.Form:
10689
res = app.get("/").follow()
10790
res.forms["select-tenant"]["tenant"] = program.country_office.pk
10891
res.forms["select-tenant"].submit()
@@ -118,14 +101,30 @@ def test_import_data_aurora(
118101
res.forms["import-aurora"]["_selected_tab"] = "aurora"
119102
res.forms["import-aurora"]["aurora-registration"] = program.projects.registrations.first().pk
120103

121-
master_detail = program.beneficiary_group.master_detail
104+
return res.forms["import-aurora"]
122105

123-
if error_expected and master_detail:
124-
with pytest.raises(ValueError, match=error_message):
125-
res.forms["import-aurora"].submit()
126-
return
127106

128-
res = res.forms["import-aurora"].submit()
107+
@pytest.mark.django_db(transaction=True)
108+
@pytest.mark.parametrize(
109+
("stub_data", "hh_count", "ind_count"),
110+
[
111+
(stub.imported["correct"], 2, 3),
112+
(stub.imported["no_individuals"], 0, 0),
113+
(stub.imported["empty_household_data"], 1, 1),
114+
(stub.imported["update_head_name"], 1, 1),
115+
],
116+
)
117+
def test_import_data_aurora_success(
118+
force_migrated_records: None,
119+
program: "CountryProgram",
120+
form_aurora: forms.Form,
121+
stub_data: dict[str, Any],
122+
hh_count: int,
123+
ind_count: int,
124+
) -> None:
125+
form_aurora.submit()
126+
127+
master_detail = program.beneficiary_group.master_detail
129128
households = program.households.all()
130129
individuals = program.individuals.all()
131130
assert individuals.count() == ind_count
@@ -137,3 +136,32 @@ def test_import_data_aurora(
137136
assert {hh.heads().first().name for hh in households} == {"John", "Jane"}
138137
elif hh_count == 1 and stub_data == stub.imported["update_head_name"]:
139138
assert households.first().name == "Doe"
139+
140+
141+
@pytest.mark.django_db(transaction=True)
142+
@pytest.mark.parametrize(
143+
("stub_data", "hh_count", "ind_count", "error_message"),
144+
[
145+
(stub.imported["multiple_households"], 0, 1, "Multiple households found"),
146+
(stub.imported["invalid_key"], 0, 0, r".*must contain an underscore"),
147+
],
148+
)
149+
def test_import_data_aurora_errors(
150+
force_migrated_records: None,
151+
program: "CountryProgram",
152+
form_aurora: forms.Form,
153+
stub_data: dict[str, Any],
154+
hh_count: int,
155+
ind_count: int,
156+
error_message: str,
157+
) -> None:
158+
master_detail = program.beneficiary_group.master_detail
159+
expected_success = stub_data == stub.imported["multiple_households"] and not master_detail
160+
161+
if expected_success:
162+
form_aurora.submit()
163+
assert program.individuals.count() == ind_count
164+
assert program.households.count() == hh_count
165+
else:
166+
with pytest.raises(ValueError, match=error_message):
167+
form_aurora.submit()

0 commit comments

Comments
 (0)