Skip to content

Commit 8252852

Browse files
authored
Merge pull request #161 from unicef/feature/262502-fail_if_alien-default-as-true
use validate with choices when import instead of checkboxes
2 parents 0a397ae + 62d55ab commit 8252852

File tree

19 files changed

+180
-108
lines changed

19 files changed

+180
-108
lines changed

src/country_workspace/contrib/aurora/forms.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
from country_workspace.contrib.aurora.models import Registration
66
from country_workspace.models import Program
7+
from country_workspace.workspaces.admin.forms import BaseImportForm
78

89

9-
class ImportAuroraForm(forms.Form):
10+
class ImportAuroraForm(BaseImportForm):
1011
batch_name = forms.CharField(required=False, help_text="Label for this batch.")
1112
registration = forms.ModelChoiceField(
1213
queryset=Registration.objects.none(),
@@ -24,12 +25,6 @@ class ImportAuroraForm(forms.Form):
2425
initial="family_name",
2526
help_text="Which Individual's column should be used as label for the household.",
2627
)
27-
check_before = forms.BooleanField(
28-
required=False, help_text="Prevent import if errors if data is not valid against data checker."
29-
)
30-
fail_if_alien = forms.BooleanField(
31-
required=False, help_text="Fails if it finds fields which do not exists in data checker."
32-
)
3328

3429
def __init__(self, *args: Any, program: Program | None = None, **kwargs: Any) -> None:
3530
super().__init__(*args, **kwargs)

src/country_workspace/contrib/aurora/pipeline.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
from country_workspace.contrib.aurora.exceptions import TooManyBeneficiaryError
77
from country_workspace.models import AsyncJob, Batch, Household, Individual
88
from country_workspace.models.household import RELATIONSHIP_HEAD, RELATIONSHIP_FIELDNAME
9-
from country_workspace.utils.config import BatchNameConfig, FailIfAlienConfig
9+
from country_workspace.utils.config import BatchNameConfig, ValidateModeConfig
1010
from country_workspace.utils.fields import clean_field_names
11+
from country_workspace.utils.types import BeneficiaryMapping
1112
from country_workspace.validators.beneficiaries import validate_beneficiaries
1213

1314

14-
class Config(BatchNameConfig, FailIfAlienConfig):
15+
class Config(BatchNameConfig, ValidateModeConfig):
1516
registration_reference_pk: str | None
1617
master_detail: bool
1718
household_column_prefix: NotRequired[str]
@@ -61,12 +62,13 @@ def import_from_aurora(job: AsyncJob) -> dict[str, int]:
6162
total["households"] += 1
6263
records_data.append((record_id, individuals))
6364

64-
validate_records(records_data, cfg)
65+
if mapping := validate_records(records_data, cfg):
66+
validate_beneficiaries(mapping, cfg, job.program.country_office)
6567

6668
return total
6769

6870

69-
def validate_records(records_data: list[tuple[int, list[Individual]]], cfg: Config) -> None:
71+
def validate_records(records_data: list[tuple[int, list[Individual]]], cfg: Config) -> BeneficiaryMapping:
7072
"""Validate beneficiaries based on configuration and record data.
7173
7274
Args:
@@ -87,9 +89,7 @@ def validate_records(records_data: list[tuple[int, list[Individual]]], cfg: Conf
8789
raise TooManyBeneficiaryError("Individual", record_id=record_id, count=len(individuals))
8890
if individuals:
8991
mapping[record_id] = individuals[0]
90-
91-
if mapping:
92-
validate_beneficiaries(cfg, mapping)
92+
return mapping
9393

9494

9595
def create_household(batch: Batch, data: dict[str, Any], prefix: str) -> Household:

src/country_workspace/contrib/kobo/forms.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,17 @@
33
from django import forms
44

55
from country_workspace.contrib.kobo.sync import make_client
6+
from country_workspace.workspaces.admin.forms import BaseImportForm
67

78

8-
class ImportKoboForm(forms.Form):
9+
class ImportKoboForm(BaseImportForm):
910
batch_name = forms.CharField(required=False, help_text="Label for this batch")
1011
project_id = forms.ChoiceField(required=True, choices=(), help_text="Select a project")
1112
individual_records_field = forms.CharField(
1213
required=False,
1314
initial="individual_questions",
1415
help_text="Which field contains individual records",
1516
)
16-
check_before = forms.BooleanField(
17-
required=False, help_text="Prevent import if errors if data is not valid against data checker."
18-
)
19-
fail_if_alien = forms.BooleanField(
20-
required=False, help_text="Fails if it finds fields which do not exists in data checker."
21-
)
2217

2318
def __init__(self, *args: Any, kobo_country_code: str | None, **kwargs: Any) -> None:
2419
super().__init__(*args, **kwargs)

src/country_workspace/contrib/kobo/sync.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
from country_workspace.contrib.kobo.api.data.submission import Submission
1515
from country_workspace.contrib.kobo.models import KoboSubmission
1616
from country_workspace.models import AsyncJob, Batch, Household, Individual
17-
from country_workspace.utils.config import BatchNameConfig, FailIfAlienConfig
17+
from country_workspace.utils.config import BatchNameConfig, ValidateModeConfig
1818
from country_workspace.utils.fields import clean_field_names, TO_UPPERCASE_FIELDS
1919
from country_workspace.utils.functional import compose
2020

2121

22-
class Config(BatchNameConfig, FailIfAlienConfig):
22+
class Config(BatchNameConfig, ValidateModeConfig):
2323
project_id: str
2424
individual_records_field: str
2525

src/country_workspace/datasources/rdi.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections.abc import Iterable, Generator
55
from enum import StrEnum
66
from typing import Any, Mapping, cast, NotRequired
7+
from functools import partial
78

89
import openpyxl
910
from PIL import Image
@@ -14,9 +15,10 @@
1415
from country_workspace.contrib.kobo.api.data.helpers import VALUE_FORMAT
1516
from country_workspace.datasources.utils import datetime_to_date, date_to_iso_string
1617
from country_workspace.models import AsyncJob, Batch, Household, Individual
17-
from country_workspace.utils.config import BatchNameConfig, FailIfAlienConfig
18+
from country_workspace.utils.config import BatchNameConfig, ValidateModeConfig
1819
from country_workspace.utils.fields import Record, clean_field_names
1920
from country_workspace.utils.functional import compose
21+
from country_workspace.utils.types import ValidateBeneficiaries
2022
from country_workspace.validators.beneficiaries import validate_beneficiaries
2123

2224
RDI = str | io.BytesIO
@@ -29,7 +31,7 @@
2931
PEOPLE = "people"
3032

3133

32-
class Config(BatchNameConfig, FailIfAlienConfig):
34+
class Config(BatchNameConfig, ValidateModeConfig):
3335
master_detail: bool
3436
household_pk_col: NotRequired[str]
3537
master_column_label: NotRequired[str]
@@ -219,20 +221,24 @@ def import_from_rdi(job: AsyncJob) -> dict[str, int]:
219221
imported_by=job.owner,
220222
source=Batch.BatchSource.RDI,
221223
)
224+
validate = partial(validate_beneficiaries, config=config, office=job.program.country_office)
222225
if config["master_detail"]:
223-
return _import_master_detail(job, batch, config)
224-
return _import_people_only(job, batch, config)
226+
return _import_master_detail(job, batch, config, validate)
227+
return _import_people_only(job, batch, config, validate)
225228

226229

227-
def _import_master_detail(job: AsyncJob, batch: Batch, config: dict) -> dict[str, int]:
230+
def _import_master_detail(
231+
job: AsyncJob, batch: Batch, config: Config, validate: ValidateBeneficiaries
232+
) -> dict[str, int]:
228233
household_sheet, individual_sheet = read_sheets(config, job.file, SheetName.HOUSEHOLDS, SheetName.INDIVIDUALS)
229234
household_mapping = process_households(household_sheet, job, batch, config)
230235
individuals_mapping = process_beneficiaries(individual_sheet, job, batch, config, household_mapping)
231-
validate_beneficiaries(config, household_mapping)
236+
validate(household_mapping)
232237
return {"household": len(household_mapping), "individual": len(individuals_mapping)}
233238

234239

235-
def _import_people_only(job: AsyncJob, batch: Batch, config: dict) -> dict[str, int]:
240+
def _import_people_only(job: AsyncJob, batch: Batch, config: Config, validate: ValidateBeneficiaries) -> dict[str, int]:
236241
(people_sheet,) = read_sheets(config, job.file, SheetName.PEOPLE)
237-
validate_beneficiaries(config, people_mapping := process_beneficiaries(people_sheet, job, batch, config))
242+
people_mapping = process_beneficiaries(people_sheet, job, batch, config)
243+
validate(people_mapping)
238244
return {"people": len(people_mapping)}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Generated by Django 5.2.3 on 2025-07-10 13:56
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("country_workspace", "0019_mappingimporter"),
9+
]
10+
11+
operations = [
12+
migrations.AlterUniqueTogether(
13+
name="rdp",
14+
unique_together={("push_date", "name")},
15+
),
16+
]

src/country_workspace/models/batch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class BatchSource(models.TextChoices):
1414
country_office = models.ForeignKey("Office", on_delete=models.CASCADE, related_name="%(class)ss")
1515
program = models.ForeignKey("Program", on_delete=models.CASCADE, related_name="%(class)ss")
1616
name = models.CharField(max_length=255, blank=True, null=True)
17-
import_date = models.DateTimeField(auto_now=True, db_index=True)
17+
import_date = models.DateTimeField(auto_now=True)
1818
imported_by = models.ForeignKey(User, on_delete=models.CASCADE)
1919
source = models.CharField(max_length=255, blank=True, null=True, choices=BatchSource.choices)
2020

src/country_workspace/models/rdp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ class PushStatus(models.TextChoices):
2020
hope_rdi_id = models.CharField(
2121
max_length=200, null=True, editable=False, help_text=_("RDI unique ID within the HOPE core.")
2222
)
23-
push_date = models.DateTimeField(auto_now=True, db_index=True)
23+
push_date = models.DateTimeField(auto_now=True)
2424
pushed_by = models.ForeignKey(User, on_delete=models.CASCADE)
2525

2626
class Meta:
27-
unique_together = (("program", "name"),)
27+
unique_together = (("push_date", "name"),)
2828
verbose_name = _("Registration Data Push")
2929
verbose_name_plural = _("Registration Data Pushes")
3030

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
from typing import TypedDict
2+
from country_workspace.workspaces.admin.forms import ValidateMode
23

34

45
class BatchNameConfig(TypedDict):
56
batch_name: str
67

78

8-
class CheckBeforeConfig(TypedDict):
9-
check_before: bool
10-
11-
12-
class FailIfAlienConfig(CheckBeforeConfig):
13-
fail_if_alien: bool
9+
class ValidateModeConfig(TypedDict):
10+
validate_mode: ValidateMode
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
from typing import TypeVar
1+
from typing import Mapping, Protocol, TypeVar
22
from country_workspace.models import Household, Individual
33

44
T_Beneficiary = TypeVar("T_Beneficiary", bound=Individual | Household)
5+
6+
type BeneficiaryMapping = Mapping[int, T_Beneficiary]
7+
8+
9+
class ValidateBeneficiaries(Protocol):
10+
def __call__(self, mapping: BeneficiaryMapping) -> None: ...

0 commit comments

Comments
 (0)