diff --git a/pyproject.toml b/pyproject.toml index e145b040bb..6b9c284d5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,7 +137,7 @@ distribution = true [project] name = "hope" -version = "2.15.0" +version = "2.16.0" description = "HCT MIS is UNICEF's humanitarian cash transfer platform." authors = [ { name = "Tivix" }, diff --git a/src/frontend/package.json b/src/frontend/package.json index 8b0ca87c11..71f65a10ae 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "2.15.0", + "version": "2.16.0", "private": true, "type": "module", "scripts": { diff --git a/src/frontend/src/components/grievances/LookUps/LookUpReassignRole/LookUpReassignRole.tsx b/src/frontend/src/components/grievances/LookUps/LookUpReassignRole/LookUpReassignRole.tsx index 65202a7b2a..f17f40daaa 100644 --- a/src/frontend/src/components/grievances/LookUps/LookUpReassignRole/LookUpReassignRole.tsx +++ b/src/frontend/src/components/grievances/LookUps/LookUpReassignRole/LookUpReassignRole.tsx @@ -119,7 +119,7 @@ export function LookUpReassignRole({ return ( el.role === IndividualRoleInHouseholdRole.Primary || el.role === 'HEAD', ); - const mappedReassignLookups = (): ReactElement => ( <> {selectedIndividualsToReassign.map((selectedIndividualToReassign) => { @@ -97,7 +96,7 @@ export function ReassignMultipleRoleBox({ ticket={ticket} household={householdAndRole.household} individualToReassign={selectedIndividualToReassign} - initialSelectedIndividualId={reassignDataDictByIndividualId[selectedIndividualToReassign.id].new_individual} + initialSelectedIndividualId={reassignDataDictByIndividualId[selectedIndividualToReassign.id]?.new_individual} /> )); @@ -137,7 +136,7 @@ export function ReassignMultipleRoleBox({ ticket={ticket} household={household} individualToReassign={selectedIndividualToReassign} - initialSelectedIndividualId={reassignDataDictByIndividualId[selectedIndividualToReassign.id].new_individual} + initialSelectedIndividualId={reassignDataDictByIndividualId[selectedIndividualToReassign.id]?.new_individual} /> )} diff --git a/src/frontend/src/containers/tables/rdi/ImportedHouseholdsTable/ImportedHouseholdTableRow.tsx b/src/frontend/src/containers/tables/rdi/ImportedHouseholdsTable/ImportedHouseholdTableRow.tsx index 4fec6547dd..9c53deeef1 100644 --- a/src/frontend/src/containers/tables/rdi/ImportedHouseholdsTable/ImportedHouseholdTableRow.tsx +++ b/src/frontend/src/containers/tables/rdi/ImportedHouseholdsTable/ImportedHouseholdTableRow.tsx @@ -57,7 +57,7 @@ export function ImportedHouseholdTableRow({ handleClick()}> - {isMerged ? household.unicefId : household.importId} + {household.unicefId} {household?.headOfHousehold?.fullName} diff --git a/src/frontend/src/containers/tables/rdi/ImportedIndividualsTable/ImportedIndividualsTableRow.tsx b/src/frontend/src/containers/tables/rdi/ImportedIndividualsTable/ImportedIndividualsTableRow.tsx index 308cce5f7a..f99dbb0c92 100644 --- a/src/frontend/src/containers/tables/rdi/ImportedIndividualsTable/ImportedIndividualsTableRow.tsx +++ b/src/frontend/src/containers/tables/rdi/ImportedIndividualsTable/ImportedIndividualsTableRow.tsx @@ -20,7 +20,6 @@ interface ImportedIndividualsTableRowProps { export function ImportedIndividualsTableRow({ individual, choices, - isMerged, rdi, }: ImportedIndividualsTableRowProps): ReactElement { const navigate = useNavigate(); @@ -56,7 +55,7 @@ export function ImportedIndividualsTableRow({ > - {isMerged ? individual.unicefId : individual.importId} + {individual.unicefId} {individual.fullName} diff --git a/src/hct_mis_api/api/endpoints/rdi/upload.py b/src/hct_mis_api/api/endpoints/rdi/upload.py index 39fc43705f..1eb1c84910 100644 --- a/src/hct_mis_api/api/endpoints/rdi/upload.py +++ b/src/hct_mis_api/api/endpoints/rdi/upload.py @@ -140,6 +140,7 @@ class Meta: "updated_at", "version", "vector_column", + "unicef_id", ] def validate_role(self, value: str) -> Optional[str]: @@ -175,6 +176,7 @@ class Meta: "geopoint", "detail_id", "version", + "unicef_id", ] validators = [HouseholdValidator()] diff --git a/src/hct_mis_api/apps/account/permissions.py b/src/hct_mis_api/apps/account/permissions.py index 3a5b197a25..93ccddc725 100644 --- a/src/hct_mis_api/apps/account/permissions.py +++ b/src/hct_mis_api/apps/account/permissions.py @@ -378,9 +378,12 @@ def check_node_permission(cls, info: Any, object_instance: Any) -> None: raise PermissionDenied("Permission Denied") @classmethod - def get_node(cls, info: Any, object_id: str) -> Optional[Model]: + def get_node(cls, info: Any, object_id: str, **kwargs: Any) -> Optional[Model]: try: - object_instance = cls._meta.model.objects.get(pk=object_id) + if "get_object_queryset" in kwargs: + object_instance = kwargs.get("get_object_queryset").get(pk=object_id) + else: + object_instance = cls.get_queryset(cls._meta.model.objects, info).get(pk=object_id) cls.check_node_permission(info, object_instance) except cls._meta.model.DoesNotExist: object_instance = None diff --git a/src/hct_mis_api/apps/household/migrations/0005_migration.py b/src/hct_mis_api/apps/household/migrations/0005_migration.py new file mode 100644 index 0000000000..836f54f346 --- /dev/null +++ b/src/hct_mis_api/apps/household/migrations/0005_migration.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.25 on 2024-12-19 11:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('household', '0004_migration'), + ] + + operations = [ + migrations.AddConstraint( + model_name='household', + constraint=models.UniqueConstraint(condition=models.Q(('is_removed', False)), fields=('unicef_id', 'program'), name='unique_hh_unicef_id_in_program'), + ), + migrations.AddConstraint( + model_name='individual', + constraint=models.UniqueConstraint(condition=models.Q(('is_removed', False), ('duplicate', False)), fields=('unicef_id', 'program'), name='unique_ind_unicef_id_in_program'), + ), + ] diff --git a/src/hct_mis_api/apps/household/models.py b/src/hct_mis_api/apps/household/models.py index ca48ab5733..0e0635e948 100644 --- a/src/hct_mis_api/apps/household/models.py +++ b/src/hct_mis_api/apps/household/models.py @@ -576,6 +576,13 @@ class CollectType(models.TextChoices): class Meta: verbose_name = "Household" permissions = (("can_withdrawn", "Can withdrawn Household"),) + constraints = [ + UniqueConstraint( + fields=["unicef_id", "program"], + condition=Q(is_removed=False), + name="unique_hh_unicef_id_in_program", + ) + ] def save(self, *args: Any, **kwargs: Any) -> None: from hct_mis_api.apps.targeting.models import ( @@ -1187,6 +1194,13 @@ def __str__(self) -> str: class Meta: verbose_name = "Individual" indexes = (GinIndex(fields=["vector_column"]),) + constraints = [ + UniqueConstraint( + fields=["unicef_id", "program"], + condition=Q(is_removed=False) & Q(duplicate=False), + name="unique_ind_unicef_id_in_program", + ) + ] def recalculate_data(self, save: bool = True) -> Tuple[Any, List[str]]: update_fields = ["disability"] diff --git a/src/hct_mis_api/apps/household/schema.py b/src/hct_mis_api/apps/household/schema.py index 95652bc139..eb05f07f73 100644 --- a/src/hct_mis_api/apps/household/schema.py +++ b/src/hct_mis_api/apps/household/schema.py @@ -5,6 +5,7 @@ Case, F, Func, + Model, OuterRef, Prefetch, Q, @@ -264,13 +265,17 @@ class IndividualNode(BaseNodePermissionMixin, AdminUrlNodeMixin, DjangoObjectTyp IndividualIdentityNode, ) + @classmethod + def get_node(cls, info: Any, object_id: str, **kwargs: Any) -> Optional[Model]: + return super().get_node(info, object_id, get_object_queryset=Individual.all_merge_status_objects) + @staticmethod def resolve_documents(parent: Individual, info: Any) -> QuerySet[Document]: - return Document.objects.filter(pk__in=parent.documents.values("id")) + return parent.documents(manager="all_merge_status_objects") @staticmethod def resolve_identities(parent: Individual, info: Any) -> QuerySet[IndividualIdentity]: - return IndividualIdentity.objects.filter(pk__in=parent.identities.values("id")) + return parent.identities(manager="all_merge_status_objects") @staticmethod def resolve_import_id(parent: Individual, info: Any) -> str: @@ -282,16 +287,16 @@ def resolve_preferred_language(parent: Individual, info: Any) -> Optional[str]: @staticmethod def resolve_payment_channels(parent: Individual, info: Any) -> QuerySet[BankAccountInfo]: - return BankAccountInfo.objects.filter(individual=parent).annotate(type=Value("BANK_TRANSFER")) + return BankAccountInfo.all_merge_status_objects.filter(individual=parent).annotate(type=Value("BANK_TRANSFER")) def resolve_bank_account_info(parent, info: Any) -> Optional[BankAccountInfo]: - bank_account_info = parent.bank_account_info.first() + bank_account_info = parent.bank_account_info(manager="all_merge_status_objects").first() # type: ignore if bank_account_info: return bank_account_info return None def resolve_role(parent, info: Any) -> str: - role = parent.households_and_roles.first() + role = parent.households_and_roles(manager="all_merge_status_objects").first() if role is not None: return role.role return ROLE_NO_ROLE @@ -481,13 +486,14 @@ def resolve_selection(parent: Household, info: Any) -> HouseholdSelection: @staticmethod def resolve_individuals(parent: Household, info: Any, *arg: Any, **kwargs: Any) -> QuerySet: - individuals_ids = list(parent.individuals.values_list("id", flat=True)) - collectors_ids = list(parent.representatives.values_list("id", flat=True)) + individuals_ids = list(parent.individuals(manager="all_merge_status_objects").values_list("id", flat=True)) + + collectors_ids = list(parent.representatives(manager="all_merge_status_objects").values_list("id", flat=True)) ids = list(set(individuals_ids + collectors_ids)) - return Individual.objects.filter(id__in=ids).prefetch_related( + return Individual.all_merge_status_objects.filter(id__in=ids).prefetch_related( Prefetch( "households_and_roles", - queryset=IndividualRoleInHousehold.objects.filter(household=parent.id), + queryset=IndividualRoleInHousehold.all_merge_status_objects.filter(household=parent.id), ) ) @@ -578,6 +584,10 @@ def get_queryset(cls, queryset: QuerySet[Household], info: Any) -> QuerySet[Hous qs = super().get_queryset(queryset, info) return qs + @classmethod + def get_node(cls, info: Any, object_id: str, **kwargs: Any) -> Optional[Model]: + return super().get_node(info, object_id, get_object_queryset=Household.all_merge_status_objects) + class Meta: model = Household filter_fields = [] @@ -706,7 +716,7 @@ def resolve_all_individuals(self, info: Any, **kwargs: Any) -> QuerySet[Individu if program and program.status == Program.DRAFT: return Individual.objects.none() - queryset = Individual.objects.all() + queryset = Individual.all_merge_status_objects.all() if does_path_exist_in_query("edges.node.household", info): queryset = queryset.select_related("household") if does_path_exist_in_query("edges.node.household.admin2", info): @@ -762,7 +772,7 @@ def resolve_all_households(self, info: Any, **kwargs: Any) -> QuerySet: if program and program.status == Program.DRAFT: return Household.objects.none() - queryset = Household.objects.all() + queryset = Household.all_merge_status_objects.all() if not user.partner.is_unicef: # Unicef partner has full access to all AdminAreas business_area_id = BusinessArea.objects.get(slug=business_area_slug).id diff --git a/src/hct_mis_api/apps/registration_datahub/mutations.py b/src/hct_mis_api/apps/registration_datahub/mutations.py index cd2147b801..e9e0befa78 100644 --- a/src/hct_mis_api/apps/registration_datahub/mutations.py +++ b/src/hct_mis_api/apps/registration_datahub/mutations.py @@ -115,15 +115,21 @@ def create_registration_data_import_for_import_program_population( pull_pictures = registration_data_import_data.pop("pull_pictures", True) screen_beneficiary = registration_data_import_data.pop("screen_beneficiary", False) import_from_program_id = registration_data_import_data.pop("import_from_program_id", None) + households_to_exclude = Household.all_merge_status_objects.filter( + program=import_to_program_id, + ).values_list("unicef_id", flat=True) households = Household.objects.filter( program_id=import_from_program_id, withdrawn=False, - ).exclude(household_collection__households__program=import_to_program_id) + ).exclude(unicef_id__in=households_to_exclude) + individuals_to_exclude = Individual.all_merge_status_objects.filter( + program=import_to_program_id, + ).values_list("unicef_id", flat=True) individuals = Individual.objects.filter( program_id=import_from_program_id, withdrawn=False, duplicate=False, - ).exclude(individual_collection__individuals__program=import_to_program_id) + ).exclude(unicef_id__in=individuals_to_exclude) created_obj_hct = RegistrationDataImport( status=RegistrationDataImport.IMPORTING, imported_by=user, diff --git a/src/hct_mis_api/apps/registration_datahub/tasks/import_program_population.py b/src/hct_mis_api/apps/registration_datahub/tasks/import_program_population.py index 95b6ad9cac..0dfad51a54 100644 --- a/src/hct_mis_api/apps/registration_datahub/tasks/import_program_population.py +++ b/src/hct_mis_api/apps/registration_datahub/tasks/import_program_population.py @@ -8,17 +8,23 @@ def import_program_population( import_from_program_id: str, import_to_program_id: str, rdi: RegistrationDataImport ) -> None: + households_to_exclude = Household.all_merge_status_objects.filter( + program=import_to_program_id, + ).values_list("unicef_id", flat=True) copy_from_households = Household.objects.filter( program=import_from_program_id, withdrawn=False, - ).exclude(household_collection__households__program_id=import_to_program_id) + ).exclude(unicef_id__in=households_to_exclude) + individuals_to_exclude = Individual.all_merge_status_objects.filter( + program=import_to_program_id, + ).values_list("unicef_id", flat=True) copy_from_individuals = ( Individual.objects.filter( program_id=import_from_program_id, withdrawn=False, duplicate=False, ) - .exclude(individual_collection__individuals__program_id=import_to_program_id) + .exclude(unicef_id__in=individuals_to_exclude) .order_by("first_registration_date") ) import_to_program = Program.objects.get(id=import_to_program_id) diff --git a/src/hct_mis_api/apps/utils/models.py b/src/hct_mis_api/apps/utils/models.py index b9d42133b0..39ad4bdf52 100644 --- a/src/hct_mis_api/apps/utils/models.py +++ b/src/hct_mis_api/apps/utils/models.py @@ -152,9 +152,7 @@ class SoftDeletableRepresentationMergeStatusModel(MergeStatusModel): class Meta: abstract = True - # objects = SoftDeletableRepresentationMergedManager(_emit_deprecation_warnings=True) - # now we use 'rdi_merge_status' field for filtering - objects = SoftDeletableRepresentationManager() + objects = SoftDeletableRepresentationMergedManager(_emit_deprecation_warnings=True) all_merge_status_objects = SoftDeletableRepresentationManager() available_objects = SoftDeletableRepresentationMergedManager() all_objects = models.Manager() diff --git a/src/hct_mis_api/one_time_scripts/fix_program_population_import_incorrect_hh_ind_relation.py b/src/hct_mis_api/one_time_scripts/fix_program_population_import_incorrect_hh_ind_relation.py deleted file mode 100644 index a265de4886..0000000000 --- a/src/hct_mis_api/one_time_scripts/fix_program_population_import_incorrect_hh_ind_relation.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db.models import F - -from hct_mis_api.apps.household.models import Household, Individual -from hct_mis_api.apps.registration_data.models import RegistrationDataImport - - -def fix_program_population_import_incorrect_hh_ind_relation() -> None: - individuals = Individual.all_objects.filter( - registration_data_import__data_source=RegistrationDataImport.PROGRAM_POPULATION, - ).exclude(household__registration_data_import=F("registration_data_import")) - - for individual in individuals: - household = Household.all_objects.filter( - registration_data_import=individual.registration_data_import, - copied_from_id=individual.household.copied_from_id, - copied_from__isnull=False, - program=individual.program, - ).first() - if household and household.unicef_id == individual.household.unicef_id: - individual.household = household - individual.save(update_fields=["household"]) diff --git a/src/hct_mis_api/one_time_scripts/remove_production_duplicates_after_enrollment.py b/src/hct_mis_api/one_time_scripts/remove_production_duplicates_after_enrollment.py deleted file mode 100644 index 7b4f5a4a5d..0000000000 --- a/src/hct_mis_api/one_time_scripts/remove_production_duplicates_after_enrollment.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging - -from django.db.models import Count - -from hct_mis_api.apps.household.models import Household - -logger = logging.getLogger(__name__) - - -def remove_production_duplicates_after_enrollment() -> None: - # Exceptions from the further rules - for household in Household.objects.filter( - id__in=[ - "8b9bf768-4837-49aa-a598-5ad3c5822ca8", - "33a7bdf0-650d-49b4-b333-c49a7eb05356", - ] - ): - household.delete(soft=False) - - households_with_duplicates = ( - Household.objects.values("unicef_id", "program") - .annotate(household_count=Count("id")) - .filter(household_count__gt=1) - .order_by("copied_from__registration_data_import") - ) - logger.info(f"Found {households_with_duplicates.count()} households with duplicates") - - for i, entry in enumerate(households_with_duplicates, 1): - unicef_id = entry["unicef_id"] - program = entry["program"] - - households = Household.objects.filter(unicef_id=unicef_id, program=program).order_by("created_at") - - # Keep the first household and delete the duplicates - first = True - households_to_remove = [] - for household in households: - if first: - first = False - continue - if household.payment_set.exists(): - logger.info(f"Skipping {household.id} because it has payments") - continue - else: - households_to_remove.append(household) - for duplicate in households_to_remove: - duplicate.delete(soft=False) - - if i % 100 == 0: - logger.info(f"Processed {i}/{households_with_duplicates.count()} households") diff --git a/tests/selenium/targeting/test_targeting.py b/tests/selenium/targeting/test_targeting.py index a4ef66b0e1..8a699c13be 100644 --- a/tests/selenium/targeting/test_targeting.py +++ b/tests/selenium/targeting/test_targeting.py @@ -164,11 +164,13 @@ def create_flexible_attribute( return flexible_attribute -def create_custom_household(observed_disability: list[str], residence_status: str = HOST) -> Household: +def create_custom_household( + observed_disability: list[str], residence_status: str = HOST, unicef_id: str = "HH-00-0000.0442" +) -> Household: program = Program.objects.get(name="Test Programm") household, _ = create_household_and_individuals( household_data={ - "unicef_id": "HH-00-0000.0442", + "unicef_id": unicef_id, "rdi_merge_status": "MERGED", "business_area": program.business_area, "program": program, @@ -187,17 +189,17 @@ def create_custom_household(observed_disability: list[str], residence_status: st @pytest.fixture def household_with_disability() -> Household: - yield create_custom_household(observed_disability=[SEEING, HEARING]) + yield create_custom_household(observed_disability=[SEEING, HEARING], unicef_id="HH-00-0000.0443") @pytest.fixture def household_without_disabilities() -> Household: - yield create_custom_household(observed_disability=[]) + yield create_custom_household(observed_disability=[], unicef_id="HH-00-0000.0444") @pytest.fixture def household_refugee() -> Household: - yield create_custom_household(observed_disability=[], residence_status=REFUGEE) + yield create_custom_household(observed_disability=[], residence_status=REFUGEE, unicef_id="HH-00-0000.0445") def get_program_with_dct_type_and_name( @@ -235,7 +237,7 @@ def create_targeting(household_without_disabilities: Household) -> TargetPopulat target_population.save() household, _ = create_household( household_args={ - "unicef_id": "HH-00-0000.0442", + "unicef_id": "HH-00-0000.0440", "business_area": program.business_area, "program": program, "residence_status": HOST, diff --git a/tests/unit/apps/household/test_models.py b/tests/unit/apps/household/test_models.py index d4522a1ed0..485a4fe534 100644 --- a/tests/unit/apps/household/test_models.py +++ b/tests/unit/apps/household/test_models.py @@ -7,7 +7,12 @@ from hct_mis_api.apps.core.utils import IDENTIFICATION_TYPE_TO_KEY_MAPPING from hct_mis_api.apps.geo.fixtures import AreaFactory, AreaTypeFactory from hct_mis_api.apps.geo.models import Country -from hct_mis_api.apps.household.fixtures import BankAccountInfoFactory, create_household +from hct_mis_api.apps.household.fixtures import ( + BankAccountInfoFactory, + HouseholdFactory, + IndividualFactory, + create_household, +) from hct_mis_api.apps.household.models import ( IDENTIFICATION_TYPE_NATIONAL_PASSPORT, IDENTIFICATION_TYPE_OTHER, @@ -30,6 +35,7 @@ def setUpTestData(cls) -> None: super().setUpTestData() create_afghanistan() cls.business_area = BusinessArea.objects.get(slug="afghanistan") + cls.program = ProgramFactory(business_area=cls.business_area) area_type_level_1 = AreaTypeFactory( name="State1", @@ -114,15 +120,21 @@ def test_remove_household(self) -> None: household2.delete(soft=False) self.assertIsNone(Household.all_objects.filter(unicef_id="HH-9191").first()) + def test_unique_unicef_id_per_program_constraint(self) -> None: + HouseholdFactory(unicef_id="HH-123", program=self.program) + HouseholdFactory(unicef_id="HH-000", program=self.program) + with self.assertRaises(IntegrityError): + HouseholdFactory(unicef_id="HH-123", program=self.program) + class TestDocument(TestCase): @classmethod def setUpTestData(cls) -> None: super().setUpTestData() call_command("loadcountries") - business_area = create_afghanistan() + cls.business_area = create_afghanistan() afghanistan = Country.objects.get(name="Afghanistan") - _, (individual,) = create_household(household_args={"size": 1, "business_area": business_area}) + _, (individual,) = create_household(household_args={"size": 1, "business_area": cls.business_area}) cls.country = afghanistan cls.individual = individual @@ -187,10 +199,9 @@ def test_create_representation_with_the_same_number(self) -> None: program_3 = ProgramFactory() program_4 = ProgramFactory() - for _program in [program_1, program_2]: - (individual_to_create, documents_to_create, _, _) = copy_individual_fast(self.individual, _program) - Individual.objects.bulk_create([individual_to_create]) - Document.objects.bulk_create(documents_to_create) + (individual_to_create, documents_to_create, _, _) = copy_individual_fast(self.individual, program_2) + Individual.objects.bulk_create([individual_to_create]) + Document.objects.bulk_create(documents_to_create) # test regular create for _program in [program_3, program_4]: @@ -209,8 +220,9 @@ def test_create_representation_with_the_same_number(self) -> None: ) # don't allow to create representations with the same document number and programs - (individual_to_create, _, _, _) = copy_individual_fast(self.individual, _program) - (created_individual_representation,) = Individual.objects.bulk_create([individual_to_create]) + _, (individual,) = create_household( + household_args={"size": 1, "business_area": self.business_area, "program": program_1} + ) with self.assertRaises(IntegrityError): with transaction.atomic(): # bulk create @@ -218,7 +230,7 @@ def test_create_representation_with_the_same_number(self) -> None: [ Document( document_number="213123", - individual=created_individual_representation, + individual=individual, country=self.country, type=document_type, status=Document.STATUS_VALID, @@ -234,7 +246,7 @@ def test_create_representation_with_the_same_number(self) -> None: # regular create Document.objects.create( document_number="213123", - individual=created_individual_representation, + individual=individual, country=self.country, type=document_type, status=Document.STATUS_VALID, @@ -421,21 +433,16 @@ def test_create_representations_duplicated_documents_with_different_numbers_and_ # allow to create representations with the same document number within different programs self.individual.is_original = True self.individual.save() - - program_1 = self.individual.program program_2 = ProgramFactory() program_3 = ProgramFactory() - # make representations with the same number - for _program in [program_1, program_2]: - (individual_to_create, documents_to_create, _, _) = copy_individual_fast(self.individual, _program) - Individual.objects.bulk_create([individual_to_create]) - Document.objects.bulk_create(documents_to_create) + # make representation with the same number + (individual_to_create, documents_to_create, _, _) = copy_individual_fast(self.individual, program_2) + Individual.objects.bulk_create([individual_to_create]) + Document.objects.bulk_create(documents_to_create) # make representation with different number - program_3_individual_representation = (individual_to_create, _, _, _) = copy_individual_fast( - self.individual, program_3 - ) + (individual_to_create, _, _, _) = copy_individual_fast(self.individual, program_3) (program_3_individual_representation,) = Individual.objects.bulk_create([individual_to_create]) Document.objects.create( document_number="456", @@ -509,8 +516,8 @@ class TestIndividualModel(TestCase): @classmethod def setUpTestData(cls) -> None: super().setUpTestData() - create_afghanistan() - ProgramFactory() + business_area = create_afghanistan() + cls.program = ProgramFactory(business_area=business_area) def test_bank_name(self) -> None: individual = create_household({"size": 1})[1][0] @@ -531,3 +538,9 @@ def test_bank_branch_name(self) -> None: individual = create_household({"size": 1})[1][0] bank_account_info = BankAccountInfoFactory(individual=individual) self.assertEqual(individual.bank_branch_name, bank_account_info.bank_branch_name) + + def test_unique_unicef_id_per_program_constraint(self) -> None: + IndividualFactory(unicef_id="IND-123", program=self.program) + IndividualFactory(unicef_id="IND-000", program=self.program) + with self.assertRaises(IntegrityError): + IndividualFactory(unicef_id="IND-123", program=self.program) diff --git a/tests/unit/apps/registration_datahub/test_program_population_to_pending_objects.py b/tests/unit/apps/registration_datahub/test_program_population_to_pending_objects.py index f57585a4a3..353d74ecf3 100644 --- a/tests/unit/apps/registration_datahub/test_program_population_to_pending_objects.py +++ b/tests/unit/apps/registration_datahub/test_program_population_to_pending_objects.py @@ -456,32 +456,27 @@ def test_create_pending_objects_from_objects(self) -> None: pending_individual_role_in_household.individual, pending_individuals.exclude(relationship=HEAD).first(), ) - for _ in range(10): - registration_data_import = RegistrationDataImportFactory( - business_area=self.afghanistan, - program=self.program_to, - ) - import_program_population( - import_from_program_id=str(self.program_from.id), - import_to_program_id=str(self.program_to.id), - rdi=registration_data_import, - ) - pending_household = Household.pending_objects.order_by("created_at").last() - pending_individual1 = Individual.pending_objects.order_by("-created_at")[0] - pending_individual2 = Individual.pending_objects.order_by("-created_at")[1] - - self.assertIn( - pending_household.head_of_household, - [pending_individual1, pending_individual2], - ) - self.assertEqual( - pending_individual1.household, - pending_household, - ) - self.assertEqual( - pending_individual2.household, - pending_household, - ) + registration_data_import = RegistrationDataImportFactory( + business_area=self.afghanistan, + program=self.program_to, + ) + import_program_population( + import_from_program_id=str(self.program_from.id), + import_to_program_id=str(self.program_to.id), + rdi=registration_data_import, + ) + pending_household_count = ( + Household.pending_objects.filter(registration_data_import=registration_data_import) + .order_by("created_at") + .count() + ) + pending_individual_count = ( + Individual.pending_objects.filter(registration_data_import=registration_data_import) + .order_by("-created_at") + .count() + ) + self.assertEqual(pending_household_count, 0) + self.assertEqual(pending_individual_count, 0) def test_not_import_excluded_objects(self) -> None: household_withdrawn, individuals = create_household_and_individuals( @@ -520,10 +515,12 @@ def test_not_import_excluded_objects(self) -> None: household_already_in_program.household_collection = household_collection household_already_in_program.save() household_already_in_program_repr.household_collection = household_collection + household_already_in_program_repr.unicef_id = household_already_in_program.unicef_id household_already_in_program_repr.save() individuals_already_in_program[0].individual_collection = individual_collection individuals_already_in_program[0].save() individuals_already_in_program_repr[0].individual_collection = individual_collection + individuals_already_in_program_repr[0].unicef_id = individuals_already_in_program[0].unicef_id individuals_already_in_program_repr[0].save() self._object_count_before_after() diff --git a/tests/unit/apps/registration_datahub/test_rdi_merge.py b/tests/unit/apps/registration_datahub/test_rdi_merge.py index a58c99baed..739d295675 100644 --- a/tests/unit/apps/registration_datahub/test_rdi_merge.py +++ b/tests/unit/apps/registration_datahub/test_rdi_merge.py @@ -447,6 +447,7 @@ def test_merge_rdi_create_collections(self, household_representation_exists: boo if False, another household representation exists, but it does not have collection, if None, household representation does not exist in another program """ + program_2 = ProgramFactory(business_area=self.rdi.business_area) self.rdi.data_source = RegistrationDataImport.PROGRAM_POPULATION self.rdi.save() imported_household = HouseholdFactory( @@ -454,11 +455,13 @@ def test_merge_rdi_create_collections(self, household_representation_exists: boo registration_data_import=self.rdi, unicef_id="HH-9", rdi_merge_status=MergeStatusModel.PENDING, + program=self.rdi.program, ) self.set_imported_individuals(imported_household) individual_without_collection = IndividualFactory( unicef_id="IND-9", business_area=self.rdi.business_area, + program=program_2, household=None, ) individual_without_collection.individual_collection = None @@ -468,6 +471,7 @@ def test_merge_rdi_create_collections(self, household_representation_exists: boo IndividualFactory( unicef_id="IND-8", business_area=self.rdi.business_area, + program=program_2, individual_collection=individual_collection, household=None, ) @@ -477,6 +481,7 @@ def test_merge_rdi_create_collections(self, household_representation_exists: boo household = HouseholdFactory( head_of_household=individual_without_collection, business_area=self.rdi.business_area, + program=program_2, unicef_id="HH-9", ) household.household_collection = None diff --git a/tests/unit/apps/registration_datahub/test_registration_program_population_import_task.py b/tests/unit/apps/registration_datahub/test_registration_program_population_import_task.py index 113474a75a..624520b69a 100644 --- a/tests/unit/apps/registration_datahub/test_registration_program_population_import_task.py +++ b/tests/unit/apps/registration_datahub/test_registration_program_population_import_task.py @@ -207,7 +207,7 @@ def test_registration_program_population_import_task(self) -> None: str(self.program_from.id), str(self.program_to.id), ) - self._imported_objects_count_after(2) + self._imported_objects_count_after(1) def test_registration_program_population_import_task_error(self) -> None: rdi_id = self.registration_data_import.id diff --git a/tests/unit/apps/targeting/snapshots/snap_test_copy_target_population_mutation.py b/tests/unit/apps/targeting/snapshots/snap_test_copy_target_population_mutation.py index dd1bcfdf15..364024e09f 100644 --- a/tests/unit/apps/targeting/snapshots/snap_test_copy_target_population_mutation.py +++ b/tests/unit/apps/targeting/snapshots/snap_test_copy_target_population_mutation.py @@ -151,7 +151,7 @@ 'status': 'OPEN', 'targetingCriteria': { 'householdIds': '', - 'individualIds': "['IND-1']", + 'individualIds': "['IND-12']", 'rules': [ ] }, diff --git a/tests/unit/apps/targeting/test_copy_target_population_mutation.py b/tests/unit/apps/targeting/test_copy_target_population_mutation.py index 21b2c081de..5ca5fcc072 100644 --- a/tests/unit/apps/targeting/test_copy_target_population_mutation.py +++ b/tests/unit/apps/targeting/test_copy_target_population_mutation.py @@ -86,7 +86,7 @@ def setUpTestData(cls) -> None: }, ) individual = individuals[0] - individual.unicef_id = "IND-1" + individual.unicef_id = "IND-12" individual.save() cls.household = household cls.update_partner_access_to_program(partner, cls.program) diff --git a/tests/unit/apps/targeting/test_targeting_validators.py b/tests/unit/apps/targeting/test_targeting_validators.py index 29d609085f..7dd0ba3e6d 100644 --- a/tests/unit/apps/targeting/test_targeting_validators.py +++ b/tests/unit/apps/targeting/test_targeting_validators.py @@ -46,17 +46,17 @@ def setUpTestData(cls) -> None: def test_TargetingCriteriaInputValidator(self) -> None: validator = TargetingCriteriaInputValidator - create_household({"unicef_id": "HH-1", "size": 1}, {"unicef_id": "IND-1"}) + create_household({"unicef_id": "HH-1", "size": 1}, {"unicef_id": "IND-12"}) self._update_program(self.program_standard) validator.validate( - {"rules": [{"Rule1": {"test": "123"}, "household_ids": "HH-1", "individual_ids": "IND-1"}]}, + {"rules": [{"Rule1": {"test": "123"}, "household_ids": "HH-1", "individual_ids": "IND-12"}]}, self.program_standard, ) with self.assertRaisesMessage(ValidationError, "Target criteria can only have individual ids"): self._update_program(self.program_standard_ind_only) validator.validate( - {"rules": [{"household_ids": "HH-1", "individual_ids": "IND-1"}]}, self.program_standard_ind_only + {"rules": [{"household_ids": "HH-1", "individual_ids": "IND-12"}]}, self.program_standard_ind_only ) with self.assertRaisesMessage(ValidationError, "There should be at least 1 rule in target criteria"): diff --git a/tests/unit/one_time_scripts/test_fix_program_population_import_incorrect_hh_ind_relation.py b/tests/unit/one_time_scripts/test_fix_program_population_import_incorrect_hh_ind_relation.py deleted file mode 100644 index dbce506524..0000000000 --- a/tests/unit/one_time_scripts/test_fix_program_population_import_incorrect_hh_ind_relation.py +++ /dev/null @@ -1,98 +0,0 @@ -from django.test import TestCase - -from hct_mis_api.apps.core.fixtures import create_afghanistan -from hct_mis_api.apps.household.fixtures import ( - HouseholdFactory, - create_household_and_individuals, -) -from hct_mis_api.apps.household.models import Household -from hct_mis_api.apps.program.fixtures import ProgramFactory -from hct_mis_api.apps.registration_data.fixtures import RegistrationDataImportFactory -from hct_mis_api.apps.registration_data.models import RegistrationDataImport -from hct_mis_api.one_time_scripts.fix_program_population_import_incorrect_hh_ind_relation import ( - fix_program_population_import_incorrect_hh_ind_relation, -) - - -class TestFixProgramPopulationIncorrectHhIndRelation(TestCase): - def test_fix_program_population_import_incorrect_hh_ind_relation(self) -> None: - business_area = create_afghanistan() - program = ProgramFactory(business_area=business_area) - copied_from_hh = HouseholdFactory(program=program) - program2 = ProgramFactory(business_area=business_area) - - rdi = RegistrationDataImportFactory(program=program2, data_source=RegistrationDataImport.PROGRAM_POPULATION) - rdi2 = RegistrationDataImportFactory(program=program2, data_source=RegistrationDataImport.PROGRAM_POPULATION) - rdi3 = RegistrationDataImportFactory(program=program2, data_source=RegistrationDataImport.PROGRAM_POPULATION) - household1, individuals1 = create_household_and_individuals( - { - "program": program2, - "unicef_id": "HH-01", - "rdi_merge_status": Household.PENDING, - "registration_data_import": rdi, - "copied_from": copied_from_hh, - }, - [ - {"rdi_merge_status": Household.PENDING}, - {"rdi_merge_status": Household.PENDING}, - ], - ) - - household2, individuals2 = create_household_and_individuals( - { - "program": program2, - "unicef_id": "HH-01", - "rdi_merge_status": Household.PENDING, - "registration_data_import": rdi2, - "copied_from": copied_from_hh, - }, - [{"rdi_merge_status": Household.PENDING}, {"rdi_merge_status": Household.PENDING}], - ) - ind2_1 = individuals2[0] - ind2_1.household = household1 - ind2_1.save() - - household3, individuals3 = create_household_and_individuals( - { - "program": program2, - "unicef_id": "HH-01", - "rdi_merge_status": Household.MERGED, - "registration_data_import": rdi3, - "copied_from": copied_from_hh, - }, - [{"rdi_merge_status": Household.MERGED}, {"rdi_merge_status": Household.MERGED}], - ) - ind3_1 = individuals3[0] - ind3_1.household = household1 - ind3_1.save() - ind3_2 = individuals3[1] - ind3_2.household = household2 - ind3_2.save() - - # check incorrect data - self.assertEqual(household1.individuals(manager="all_objects").count(), 4) - self.assertEqual(household2.individuals(manager="all_objects").count(), 2) - self.assertEqual(household3.individuals(manager="all_objects").count(), 0) - - self.assertNotEqual(ind2_1.household.registration_data_import, household2.registration_data_import) - self.assertNotEqual(ind3_1.household.registration_data_import, household3.registration_data_import) - self.assertNotEqual(ind3_2.household.registration_data_import, household3.registration_data_import) - - fix_program_population_import_incorrect_hh_ind_relation() - - ind2_1.refresh_from_db() - ind3_1.refresh_from_db() - ind3_2.refresh_from_db() - - self.assertEqual(household1.individuals(manager="all_objects").count(), 2) - - self.assertEqual(ind2_1.household, household2) - self.assertEqual(household2.individuals(manager="all_objects").count(), 2) - - self.assertEqual(ind3_1.household, household3) - self.assertEqual(ind3_2.household, household3) - self.assertEqual(household3.individuals(manager="all_objects").count(), 2) - - self.assertEqual(ind2_1.household.registration_data_import, household2.registration_data_import) - self.assertEqual(ind3_1.household.registration_data_import, household3.registration_data_import) - self.assertEqual(ind3_2.household.registration_data_import, household3.registration_data_import) diff --git a/tests/unit/one_time_scripts/test_remove_production_duplicates_after_enrollment.py b/tests/unit/one_time_scripts/test_remove_production_duplicates_after_enrollment.py deleted file mode 100644 index 0edcc9d0a0..0000000000 --- a/tests/unit/one_time_scripts/test_remove_production_duplicates_after_enrollment.py +++ /dev/null @@ -1,103 +0,0 @@ -from django.test import TestCase - -from hct_mis_api.apps.core.fixtures import create_afghanistan -from hct_mis_api.apps.household.fixtures import create_household_and_individuals -from hct_mis_api.apps.household.models import Household, Individual -from hct_mis_api.apps.payment.fixtures import PaymentFactory -from hct_mis_api.apps.program.fixtures import ProgramFactory -from hct_mis_api.one_time_scripts.remove_production_duplicates_after_enrollment import ( - remove_production_duplicates_after_enrollment, -) - - -class TestRemoveProductionDuplicatesAfterEnrollment(TestCase): - def test_remove_production_duplicates_after_enrollment(self) -> None: - business_area = create_afghanistan() - program = ProgramFactory(business_area=business_area) - program2 = ProgramFactory(business_area=business_area) - hh_unicef_id = "HH-20-0000.0001" - household_special_case1, individuals_special_case1 = create_household_and_individuals( - household_data={ - "id": "8b9bf768-4837-49aa-a598-5ad3c5822ca8", - "unicef_id": hh_unicef_id, - "business_area": program.business_area, - "program": program, - }, - individuals_data=[{}], - ) - household_special_case2, individuals_special_case2 = create_household_and_individuals( - household_data={ - "id": "33a7bdf0-650d-49b4-b333-c49a7eb05356", - "unicef_id": hh_unicef_id, - "business_area": program.business_area, - "program": program, - }, - individuals_data=[{}], - ) - household1, individuals1 = create_household_and_individuals( - household_data={ - "unicef_id": hh_unicef_id, - "business_area": program.business_area, - "program": program, - }, - individuals_data=[{}, {}], - ) - household2, individuals2 = create_household_and_individuals( - household_data={ - "unicef_id": hh_unicef_id, - "business_area": program.business_area, - "program": program, - }, - individuals_data=[{}, {}], - ) - household3, individuals3 = create_household_and_individuals( - household_data={ - "unicef_id": hh_unicef_id, - "business_area": program.business_area, - "program": program, - }, - individuals_data=[{}], - ) - PaymentFactory(household=household3) - - household4, individuals4 = create_household_and_individuals( - household_data={ - "unicef_id": hh_unicef_id, - "business_area": program.business_area, - "program": program, - }, - individuals_data=[{}], - ) - household_from_another_program, individuals_from_another_program = create_household_and_individuals( - household_data={ - "unicef_id": hh_unicef_id, - "business_area": program2.business_area, - "program": program2, - }, - individuals_data=[{}], - ) - - remove_production_duplicates_after_enrollment() - - self.assertIsNotNone(Household.all_objects.filter(id=household1.id).first()) - self.assertIsNotNone(Individual.all_objects.filter(id=individuals1[0].id).first()) - self.assertIsNotNone(Individual.all_objects.filter(id=individuals1[1].id).first()) - - self.assertIsNone(Household.all_objects.filter(id=household2.id).first()) - self.assertIsNone(Individual.all_objects.filter(id=individuals2[0].id).first()) - self.assertIsNone(Individual.all_objects.filter(id=individuals2[1].id).first()) - - self.assertIsNotNone(Individual.all_objects.filter(id=individuals3[0].id).first()) - self.assertIsNotNone(Household.all_objects.filter(id=household3.id).first()) - - self.assertIsNone(Household.all_objects.filter(id=household4.id).first()) - self.assertIsNone(Individual.all_objects.filter(id=individuals4[0].id).first()) - - self.assertIsNotNone(Household.all_objects.filter(id=household_from_another_program.id).first()) - self.assertIsNotNone(Individual.all_objects.filter(id=individuals_from_another_program[0].id).first()) - - self.assertIsNone(Household.all_objects.filter(id=household_special_case1.id).first()) - self.assertIsNone(Individual.all_objects.filter(id=individuals_special_case1[0].id).first()) - - self.assertIsNone(Household.all_objects.filter(id=household_special_case2.id).first()) - self.assertIsNone(Individual.all_objects.filter(id=individuals_special_case2[0].id).first())