Skip to content

Commit 42d0900

Browse files
chg ! apply mapping at import instead of validation
1 parent a09fa27 commit 42d0900

File tree

6 files changed

+37
-65
lines changed

6 files changed

+37
-65
lines changed

src/country_workspace/contrib/hope/push.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from country_workspace.exceptions import RemoteError
1515
from country_workspace.models import AsyncJob, Rdp
1616
from country_workspace.workspaces.models import CountryHousehold, CountryIndividual
17-
from country_workspace.utils.fields import map_fields
1817

1918

2019
type Beneficiary = CountryHousehold | CountryIndividual
@@ -195,9 +194,9 @@ def prepare_batch(self) -> tuple[list[int], list[dict]]:
195194
for item in self.queryset:
196195
ids.append(item.id)
197196
data.append(
198-
{**map_fields(item.flex_fields), "members": [map_fields(m.flex_fields) for m in item.members.all()]}
197+
{**item.flex_fields, "members": [m.flex_fields for m in item.members.all()]}
199198
if self.master_detail
200-
else map_fields(item.flex_fields)
199+
else item.flex_fields
201200
)
202201
return ids, data
203202

src/country_workspace/datasources/rdi.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,15 @@ def process_households(sheet: Sheet, job: AsyncJob, batch: Batch, config: Config
100100
for i, row in enumerate(sheet, 1):
101101
household_key = get_value(row, config["household_pk_col"])
102102
label = get_value(row, config["detail_column_label"])
103+
flex_fields = job.program.apply_mapping_importer(Household, clean_field_names(row))
103104

104105
try:
105106
mapping[household_key] = cast(
106107
"Household",
107108
job.program.households.create(
108109
batch=batch,
109110
name=label,
110-
flex_fields=clean_field_names(row),
111+
flex_fields=flex_fields,
111112
),
112113
)
113114
except Exception as e:
@@ -116,35 +117,35 @@ def process_households(sheet: Sheet, job: AsyncJob, batch: Batch, config: Config
116117
return mapping
117118

118119

119-
def full_name_column(row: Record) -> str | None:
120-
for key in row:
121-
if key.startswith("full") and "name" in key:
122-
return key
123-
return None
120+
def normalize_row_structure(row: Record, people_column_prefix: str | None = None) -> tuple[Record, str | None]:
121+
if people_column_prefix:
122+
row = {k.removeprefix(people_column_prefix): v for k, v in row.items()}
123+
name_column = next((key for key in row if key.startswith("full") and "name" in key), None)
124+
return row, name_column
125+
126+
127+
def get_hh_for_ind(
128+
cleaned_row: dict, master_column_label: str, household_mapping: Mapping[int, Household] | None
129+
) -> Household | None:
130+
if not household_mapping or not master_column_label:
131+
return None
132+
household_key = get_value(cleaned_row, master_column_label)
133+
return household_mapping.get(household_key)
124134

125135

126136
def process_beneficiaries(
127137
sheet: Sheet, job: AsyncJob, batch: Batch, config: Config, household_mapping: Mapping[int, Household] | None = None
128138
) -> Mapping[int, Individual]:
129-
mapping, name_column = {}, None
130-
pp_column_prefix = config.get("people_column_prefix")
131-
is_people_only_mode = household_mapping is None
139+
mapping = {}
140+
people_column_prefix = config.get("people_column_prefix") if household_mapping is None else None
141+
master_column_label = config.get("master_column_label") if household_mapping is not None else None
142+
sheet_name = PEOPLE if household_mapping is None else INDIVIDUAL
132143

133144
for i, row in enumerate(sheet, 1):
134-
cleaned_row = (
135-
{k.removeprefix(pp_column_prefix): v for k, v in row.items()}
136-
if is_people_only_mode and pp_column_prefix
137-
else row
138-
)
139-
140-
if name_column is None:
141-
name_column = full_name_column(cleaned_row)
142-
name = get_value(cleaned_row, name_column) if name_column else None
143-
144-
household = None
145-
if not is_people_only_mode:
146-
household_key = get_value(cleaned_row, config["master_column_label"])
147-
household = household_mapping.get(household_key)
145+
cleaned_row, name_column = normalize_row_structure(row, people_column_prefix)
146+
name = cleaned_row.get(name_column) if name_column else None
147+
household = get_hh_for_ind(cleaned_row, master_column_label, household_mapping)
148+
flex_fields = job.program.apply_mapping_importer(Individual, clean_field_names(cleaned_row))
148149

149150
try:
150151
mapping[i] = cast(
@@ -153,11 +154,10 @@ def process_beneficiaries(
153154
batch=batch,
154155
name=name,
155156
household=household,
156-
flex_fields=clean_field_names(cleaned_row),
157+
flex_fields=flex_fields,
157158
),
158159
)
159160
except Exception as e:
160-
sheet_name = PEOPLE if is_people_only_mode else INDIVIDUAL
161161
raise SheetProcessingError(sheet_name, i) from e
162162

163163
return mapping

src/country_workspace/models/base.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from typing import TYPE_CHECKING, Any
22

33
from concurrency.fields import IntegerVersionField
4-
from contextlib import suppress
5-
from django.core.exceptions import ObjectDoesNotExist
64
from django.db import models
75
from django.urls import reverse
86
from django.utils import timezone
@@ -108,11 +106,6 @@ def checker(self) -> "DataChecker":
108106
raise NotImplementedError
109107

110108
def validate_with_checker(self, fail_if_alien: bool = False) -> bool:
111-
# skip if mappingimporter not found
112-
with suppress(ObjectDoesNotExist):
113-
self.checker.mappingimporter.apply(self.flex_fields)
114-
self.save(update_fields=["flex_fields"])
115-
116109
errors = self.checker.validate([self.flex_fields], fail_if_alien=fail_if_alien)
117110
if errors:
118111
self.errors = errors[1]

src/country_workspace/models/program.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import TYPE_CHECKING
2+
from contextlib import suppress
23

4+
from django.core.exceptions import ObjectDoesNotExist
35
from django.db import models
46
from django.utils.translation import gettext as _
57
from hope_flex_fields.models import DataChecker
@@ -120,3 +122,11 @@ def get_checker_for(self, m: type[Validable] | Validable) -> DataChecker:
120122
if isinstance(m, (Individual | CountryIndividual)) or m in (Individual, CountryIndividual):
121123
return self.individual_checker
122124
raise ValueError(m)
125+
126+
def apply_mapping_importer(
127+
self, m: type[Validable] | Validable, data: dict[str, str | int | bool]
128+
) -> dict[str, str | int | bool]:
129+
# skip if mapping importer not found
130+
with suppress(ObjectDoesNotExist):
131+
self.get_checker_for(m).mappingimporter.apply(data)
132+
return data

src/country_workspace/utils/fields.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
TO_REMOVE_VALUES = "_h_c", "_h_f", "_i_c", "_i_f"
1717
TO_UPPERCASE_FIELDS = "relationship", "gender", "residence_status", "consent_sharing"
18-
TO_MAP_FIELDS = {"gender": "sex"}
1918

2019

2120
def clean_field_name(v: str) -> str:
@@ -61,20 +60,6 @@ def uppercase_field_value(k: str, v: Any, fields_to_uppercase: Iterable[str] = T
6160
return v.upper() if isinstance(v, str) and any(k.startswith(prefix) for prefix in fields_to_uppercase) else v
6261

6362

64-
def map_fields(fields: dict[str, str]) -> dict[str, str]:
65-
"""
66-
Map keys in a dictionary to alternative names based on a predefined mapping.
67-
68-
Args:
69-
fields (dict[str, str]): A dictionary containing field names as keys and their values.
70-
71-
Returns:
72-
dict[str, str]: A new dictionary with keys mapped according to the predefined mapping.
73-
74-
"""
75-
return {TO_MAP_FIELDS.get(k, k): v for k, v in fields.items() if v is not None}
76-
77-
7863
def extract_uuid(value: str, prefix: str | None = None) -> UUID:
7964
"""Extract a UUID from the given string.
8065

tests/utils/test_utils_fields.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
clean_field_name,
1313
TO_REMOVE_VALUES,
1414
clean_field_names,
15-
map_fields,
1615
extract_uuid,
1716
)
1817
from country_workspace.utils.flex_fields import (
@@ -44,20 +43,6 @@ def test_clean_field_names(mocker: MockerFixture) -> None:
4443
clean_field_name_mock.assert_called_once_with(key)
4544

4645

47-
@pytest.mark.parametrize(
48-
("input_fields", "expected_output"),
49-
[
50-
({"gender": "male"}, {"sex": "male"}),
51-
({"name": "John"}, {"name": "John"}),
52-
({}, {}),
53-
({"gender": "female", "age": "30"}, {"sex": "female", "age": "30"}),
54-
],
55-
)
56-
def test_map_fields(input_fields, expected_output):
57-
result = map_fields(input_fields)
58-
assert result == expected_output
59-
60-
6146
@pytest.mark.parametrize("value", [None, "", "test"])
6247
def test_base64_image_input(value: str | None) -> None:
6348
input_ = Base64ImageInput()

0 commit comments

Comments
 (0)