Skip to content

Commit 05031af

Browse files
populate biometric_deduplication_enabled during program sync
1 parent b08f554 commit 05031af

File tree

5 files changed

+41
-58
lines changed

5 files changed

+41
-58
lines changed

src/country_workspace/admin/program.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class ProgramAdmin(SyncAdminMixin, BaseModelAdmin):
3838
"sector",
3939
"enabled",
4040
"beneficiary_group",
41+
"biometric_deduplication_enabled",
4142
"beneficiary_validator",
4243
"household_checker",
4344
"individual_checker",

src/country_workspace/contrib/hope/sync/base.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from django.contrib.contenttypes.models import ContentType
99
from django.core.cache import cache
10-
from django.db import DatabaseError
10+
from django.db import DatabaseError, transaction
1111
from django.db.models import Model
1212

1313
from country_workspace.exceptions import RemoteError
@@ -160,22 +160,22 @@ def sync_entity[T: Model](config: SyncConfig[T], client: HopeClient | None = Non
160160
stats = stats or Stats(errors=[], add=0, upd=0)
161161
client = client or HopeClient()
162162

163-
with cache.lock(f"sync-{model_name}"):
163+
with cache.lock(f"hope-sync:{model_name}"):
164164
logger.info(format_msg("SYNC_START", entity=model_name))
165165
for record in safe_get(client, config["endpoint"], stats):
166166
if not (reference_id_val := validated_reference_id(record)):
167167
continue
168168
if should_process and not should_process(record):
169169
continue
170170
try:
171-
defaults = prepare_defaults(record) if prepare_defaults else None
172-
if defaults is None or not defaults:
171+
if not (defaults := prepare_defaults(record)):
173172
continue
174-
instance, created = model.objects.update_or_create(
175-
defaults=defaults, **{reference_id: reference_id_val}
176-
)
177-
if post_process:
178-
post_process(instance, created)
173+
with transaction.atomic():
174+
instance, created = model.objects.update_or_create(
175+
defaults=defaults, **{reference_id: reference_id_val}
176+
)
177+
if post_process:
178+
post_process(instance, created)
179179
stats["add" if created else "upd"] += 1
180180
except SkipRecordError as e:
181181
logger.warning(format_msg("RECORD_SKIPPED", reference_id_val=reference_id_val, error=str(e)))
@@ -184,7 +184,7 @@ def sync_entity[T: Model](config: SyncConfig[T], client: HopeClient | None = Non
184184
add_error(stats, error)
185185
logger.error(error)
186186
if stats["errors"]:
187-
raise HopeSyncError(stats["errors"])
187+
raise HopeSyncError("\n".join(stats["errors"]))
188188
SyncLog.objects.register_sync(model)
189189
logger.info(format_msg("SYNC_COMPLETE", entity=model_name, result=stats, errors_count=0))
190190
return stats

src/country_workspace/contrib/hope/sync/context_programs.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,17 @@
3333
"member_label_plural",
3434
"master_detail",
3535
)
36-
Program_FIELDS = "name", "code", "status", "sector", "country_office", "beneficiary_group"
3736

3837
HOPE_ID = "hope_id"
3938
BUSINESS_AREAS = "business-areas"
4039
BENEFICIARY_GROUPS = "beneficiary-groups"
4140
PROGRAMS = "programs"
4241

4342

44-
def get_default_checkers() -> Mapping[str, DataChecker]:
45-
return {
46-
name: DataChecker.objects.filter(name=const_name).first()
47-
for name, const_name in (
48-
("hh", HOUSEHOLD_CHECKER_NAME),
49-
("ind", INDIVIDUAL_CHECKER_NAME),
50-
("ppl", PEOPLE_CHECKER_NAME),
51-
)
52-
}
43+
def get_default_checkers() -> Mapping[str, DataChecker | None]:
44+
target_to_checker_name = {"hh": HOUSEHOLD_CHECKER_NAME, "ind": INDIVIDUAL_CHECKER_NAME, "ppl": PEOPLE_CHECKER_NAME}
45+
checker_by_name = {c.name: c for c in DataChecker.objects.filter(name__in=target_to_checker_name.values())}
46+
return {k: checker_by_name.get(v) for k, v in target_to_checker_name.items()}
5347

5448

5549
def get_field_extractor(fields: tuple[str, ...]) -> Callable[[dict[str, Any]], dict[str, Any]]:
@@ -109,13 +103,13 @@ def prepare_program_defaults(record: dict[str, Any]) -> dict[str, Any] | None:
109103
raise SkipRecordError("Beneficiary group not found") from e
110104

111105
return {
112-
"name": record["name"],
106+
"biometric_deduplication_enabled": record["biometric_deduplication_enabled"],
113107
"code": record["programme_code"],
114-
"status": record["status"],
108+
"name": record["name"],
115109
"sector": record["sector"],
116-
"biometric_deduplication_enabled": record["biometric_deduplication_enabled"],
117-
"country_office": office,
110+
"status": record["status"],
118111
"beneficiary_group": bg,
112+
"country_office": office,
119113
}
120114

121115

@@ -151,13 +145,12 @@ def post_process_program(program: Program, created: bool) -> None:
151145
default_checkers = get_default_checkers()
152146
update_fields: list[str] = []
153147

154-
if default_checkers:
155-
program.household_checker = default_checkers.get("hh")
156-
program.individual_checker = (
157-
default_checkers.get("ind") if program.beneficiary_group.master_detail else default_checkers.get("ppl")
158-
)
159-
if program.household_checker or program.individual_checker:
160-
update_fields.extend(["household_checker", "individual_checker"])
148+
program.household_checker = default_checkers.get("hh")
149+
program.individual_checker = (
150+
default_checkers.get("ind") if program.beneficiary_group.master_detail else default_checkers.get("ppl")
151+
)
152+
if program.household_checker or program.individual_checker:
153+
update_fields.extend(["household_checker", "individual_checker"])
161154

162155
hh_ignored = get_default_ignored_fields("hh")
163156
if hh_ignored:

src/country_workspace/migrations/0043_program_biometric_deduplication_enabled.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

tests/contrib/hope/test_context_programs.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,15 @@ def get_field_extractor_mock(mocker: MockerFixture) -> Mock:
4949

5050

5151
def test_get_default_checkers(mocker: MockerFixture) -> None:
52-
data_checker_model_mock = mocker.patch("country_workspace.contrib.hope.sync.context_programs.DataChecker")
52+
dc = mocker.patch("country_workspace.contrib.hope.sync.context_programs.DataChecker")
53+
5354
assert get_default_checkers().keys() == {"hh", "ind", "ppl"}
54-
data_checker_model_mock.objects.filter.assert_any_call(name=HOUSEHOLD_CHECKER_NAME)
55-
data_checker_model_mock.objects.filter.assert_any_call(name=INDIVIDUAL_CHECKER_NAME)
56-
data_checker_model_mock.objects.filter.assert_any_call(name=PEOPLE_CHECKER_NAME)
55+
56+
assert set(dc.objects.filter.call_args.kwargs["name__in"]) == {
57+
HOUSEHOLD_CHECKER_NAME,
58+
INDIVIDUAL_CHECKER_NAME,
59+
PEOPLE_CHECKER_NAME,
60+
}
5761

5862

5963
def test_get_field_extractor() -> None:
@@ -155,24 +159,26 @@ def test_prepare_program_defaults_all_found(mocker: MockerFixture) -> None:
155159
get_office_mock = mocker.patch.object(Office.objects, "get")
156160
get_group_mock = mocker.patch.object(BeneficiaryGroup.objects, "get")
157161
record = {
162+
"beneficiary_group": (bg := "bg"),
163+
"biometric_deduplication_enabled": (dedup_enabled := True),
158164
"business_area_code": (area_code := "area_code"),
159-
"beneficiary_group": (group := "group"),
160165
"name": (name := "name"),
161166
"programme_code": (programme_code := "programme_code"),
162-
"status": (status := "status"),
163167
"sector": (sector := "sector"),
168+
"status": (status := "status"),
164169
}
165170

166171
assert prepare_program_defaults(record) == {
167-
"name": name,
172+
"biometric_deduplication_enabled": dedup_enabled,
168173
"code": programme_code,
169-
"status": status,
174+
"name": name,
170175
"sector": sector,
171-
"country_office": get_office_mock.return_value,
176+
"status": status,
172177
"beneficiary_group": get_group_mock.return_value,
178+
"country_office": get_office_mock.return_value,
173179
}
174180
get_office_mock.assert_called_once_with(code=area_code)
175-
get_group_mock.assert_called_once_with(hope_id=group)
181+
get_group_mock.assert_called_once_with(hope_id=bg)
176182

177183

178184
def test_get_default_ignored_fields_collects_from_all_sources() -> None:

0 commit comments

Comments
 (0)