diff --git a/backend/hct_mis_api/apps/core/migrations/0085_migration.py b/backend/hct_mis_api/apps/core/migrations/0085_migration.py new file mode 100644 index 0000000000..e13e9a19c0 --- /dev/null +++ b/backend/hct_mis_api/apps/core/migrations/0085_migration.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-08-19 15:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0084_migration'), + ] + + operations = [ + migrations.AlterField( + model_name='periodicfielddata', + name='subtype', + field=models.CharField(choices=[('DATE', 'Date'), ('DECIMAL', 'Number'), ('STRING', 'Text'), ('BOOL', 'Boolean (true/false)')], max_length=16), + ), + ] \ No newline at end of file diff --git a/backend/hct_mis_api/apps/core/models.py b/backend/hct_mis_api/apps/core/models.py index 3194215965..6dcecb5125 100644 --- a/backend/hct_mis_api/apps/core/models.py +++ b/backend/hct_mis_api/apps/core/models.py @@ -324,13 +324,13 @@ class PeriodicFieldData(models.Model): STRING = "STRING" DECIMAL = "DECIMAL" DATE = "DATE" - BOOLEAN = "BOOLEAN" + BOOL = "BOOL" TYPE_CHOICES = Choices( (DATE, _("Date")), (DECIMAL, _("Number")), (STRING, _("Text")), - (BOOLEAN, _("Boolean (true/false)")), + (BOOL, _("Boolean (true/false)")), ) subtype = models.CharField(max_length=16, choices=TYPE_CHOICES) diff --git a/backend/hct_mis_api/apps/core/schema.py b/backend/hct_mis_api/apps/core/schema.py index eb65970835..8332e3a437 100644 --- a/backend/hct_mis_api/apps/core/schema.py +++ b/backend/hct_mis_api/apps/core/schema.py @@ -292,7 +292,9 @@ def get_fields_attr_generators( flex_field: Optional[bool] = None, business_area_slug: Optional[str] = None, program_id: Optional[str] = None ) -> Generator: if flex_field is not False: - yield from FlexibleAttribute.objects.exclude(type=FlexibleAttribute.PDU).order_by("created_at") + yield from FlexibleAttribute.objects.filter(Q(program__isnull=True) | Q(program__id=program_id)).order_by( + "created_at" + ) if flex_field is not True: if program_id and Program.objects.get(id=program_id).is_social_worker_program: yield from FieldFactory.from_only_scopes([Scope.XLSX_PEOPLE, Scope.TARGETING]).filtered_by_types( diff --git a/backend/hct_mis_api/apps/core/tests/snapshots/snap_test_schema.py b/backend/hct_mis_api/apps/core/tests/snapshots/snap_test_schema.py index 2717b38034..57a114f558 100644 --- a/backend/hct_mis_api/apps/core/tests/snapshots/snap_test_schema.py +++ b/backend/hct_mis_api/apps/core/tests/snapshots/snap_test_schema.py @@ -77,7 +77,7 @@ }, { 'displayName': 'Boolean (true/false)', - 'value': 'BOOLEAN' + 'value': 'BOOL' } ] } diff --git a/backend/hct_mis_api/apps/core/tests/test_schema.py b/backend/hct_mis_api/apps/core/tests/test_schema.py index a2dafa75a1..6d67efc99a 100644 --- a/backend/hct_mis_api/apps/core/tests/test_schema.py +++ b/backend/hct_mis_api/apps/core/tests/test_schema.py @@ -117,7 +117,7 @@ def setUpTestData(cls) -> None: # Create a PDU field for a different program other_program = ProgramFactory(business_area=cls.business_area, status=Program.ACTIVE, name="Other Program") pdu_data_different_program = PeriodicFieldDataFactory( - subtype=PeriodicFieldData.BOOLEAN, + subtype=PeriodicFieldData.BOOL, number_of_rounds=1, rounds_names=["Round 1"], ) diff --git a/backend/hct_mis_api/apps/payment/tests/test_payment_plan_reconciliation.py b/backend/hct_mis_api/apps/payment/tests/test_payment_plan_reconciliation.py index b946ae8044..d40e25f7dd 100644 --- a/backend/hct_mis_api/apps/payment/tests/test_payment_plan_reconciliation.py +++ b/backend/hct_mis_api/apps/payment/tests/test_payment_plan_reconciliation.py @@ -379,7 +379,7 @@ def test_receiving_reconciliations_from_fsp(self, mock_get_exchange_rate: Any) - "comparisonMethod": "EQUALS", "arguments": ["True"], "fieldName": "consent", - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", } ], "individualsFiltersBlocks": [], diff --git a/backend/hct_mis_api/apps/payment/tests/test_recalculating_household_cash_received.py b/backend/hct_mis_api/apps/payment/tests/test_recalculating_household_cash_received.py index c3e7300652..6ad66b42c8 100644 --- a/backend/hct_mis_api/apps/payment/tests/test_recalculating_household_cash_received.py +++ b/backend/hct_mis_api/apps/payment/tests/test_recalculating_household_cash_received.py @@ -145,7 +145,7 @@ def setUpTestData(cls) -> None: { "comparisonMethod": "EQUALS", "fieldName": "consent", - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", "arguments": [True], } ], diff --git a/backend/hct_mis_api/apps/periodic_data_update/service/flexible_attribute_service.py b/backend/hct_mis_api/apps/periodic_data_update/service/flexible_attribute_service.py index 4e297360af..97107493c1 100644 --- a/backend/hct_mis_api/apps/periodic_data_update/service/flexible_attribute_service.py +++ b/backend/hct_mis_api/apps/periodic_data_update/service/flexible_attribute_service.py @@ -75,7 +75,7 @@ def update_pdu_flex_attributes(self) -> None: self.delete_pdu_flex_attributes(flexible_attribute_ids_to_preserve=flexible_attribute_ids_to_preserve) def update_pdu_flex_attributes_in_program_update(self) -> None: - if self.program.registration_imports.exists(): + if self.program.registration_imports.exists() or self.program.targetpopulation_set.exists(): self.increase_pdu_rounds_for_program_with_rdi() else: self.update_pdu_flex_attributes() @@ -116,6 +116,6 @@ def _validate_pdu_data_for_program_with_rdi(pdu_data_object: PeriodicFieldData, new_number_of_rounds = pdu_data["number_of_rounds"] new_rounds_names = pdu_data["rounds_names"] if new_number_of_rounds <= current_number_of_rounds: - raise GraphQLError("It is not possible to decrease the number of rounds for a Program with RDI") + raise GraphQLError("It is not possible to decrease the number of rounds for a Program with RDI or TP") if current_rounds_names != new_rounds_names[:current_number_of_rounds]: - raise GraphQLError("It is not possible to change the names of existing rounds for a Program with RDI") + raise GraphQLError("It is not possible to change the names of existing rounds for a Program with RDI or TP") diff --git a/backend/hct_mis_api/apps/periodic_data_update/service/periodic_data_update_import_service.py b/backend/hct_mis_api/apps/periodic_data_update/service/periodic_data_update_import_service.py index 77b8c16736..c3f0eb8a89 100644 --- a/backend/hct_mis_api/apps/periodic_data_update/service/periodic_data_update_import_service.py +++ b/backend/hct_mis_api/apps/periodic_data_update/service/periodic_data_update_import_service.py @@ -285,7 +285,7 @@ def _get_form_field_for_value(self, flexible_attribute: FlexibleAttribute) -> fo return forms.CharField(required=False) elif flexible_attribute.pdu_data.subtype == PeriodicFieldData.DECIMAL: return forms.FloatField(required=False) - elif flexible_attribute.pdu_data.subtype == PeriodicFieldData.BOOLEAN: + elif flexible_attribute.pdu_data.subtype == PeriodicFieldData.BOOL: return StrictBooleanField(required=False) elif flexible_attribute.pdu_data.subtype == PeriodicFieldData.DATE: return forms.DateField(required=False) diff --git a/backend/hct_mis_api/apps/periodic_data_update/tests/test_periodic_data_update_import_service.py b/backend/hct_mis_api/apps/periodic_data_update/tests/test_periodic_data_update_import_service.py index 9ef6d4cd4a..fcc2299834 100644 --- a/backend/hct_mis_api/apps/periodic_data_update/tests/test_periodic_data_update_import_service.py +++ b/backend/hct_mis_api/apps/periodic_data_update/tests/test_periodic_data_update_import_service.py @@ -88,7 +88,7 @@ def setUpTestData(cls) -> None: ) cls.boolean_attribute = create_pdu_flexible_attribute( label="Boolean Attribute", - subtype=PeriodicFieldData.BOOLEAN, + subtype=PeriodicFieldData.BOOL, number_of_rounds=1, rounds_names=["May"], program=cls.program, diff --git a/backend/hct_mis_api/apps/periodic_data_update/utils.py b/backend/hct_mis_api/apps/periodic_data_update/utils.py index 3780ba9255..419993d20d 100644 --- a/backend/hct_mis_api/apps/periodic_data_update/utils.py +++ b/backend/hct_mis_api/apps/periodic_data_update/utils.py @@ -13,7 +13,9 @@ def field_label_to_field_name(input_string: str) -> str: """ input_string = input_string.replace(" ", "_") - input_string = re.sub(r"[^\w\s-]", "", input_string) + input_string = re.sub(r"[^\w]", "", input_string) + input_string = re.sub(r"__+", "_", input_string) + input_string = input_string.strip("_") return input_string.lower() diff --git a/backend/hct_mis_api/apps/program/schema.py b/backend/hct_mis_api/apps/program/schema.py index 26de660aed..f9af2c10a3 100644 --- a/backend/hct_mis_api/apps/program/schema.py +++ b/backend/hct_mis_api/apps/program/schema.py @@ -114,6 +114,7 @@ class ProgramNode(BaseNodePermissionMixin, AdminUrlNodeMixin, DjangoObjectType): partners = graphene.List(PartnerNode) is_social_worker_program = graphene.Boolean() pdu_fields = graphene.List(PeriodicFieldNode) + target_populations_count = graphene.Int() cycles = DjangoFilterConnectionField(ProgramCycleNode, filterset_class=ProgramCycleFilter) class Meta: @@ -151,6 +152,10 @@ def resolve_is_social_worker_program(program: Program, info: Any, **kwargs: Any) def resolve_pdu_fields(program: Program, info: Any, **kwargs: Any) -> QuerySet: return program.pdu_fields.order_by("name") + @staticmethod + def resolve_target_populations_count(program: Program, info: Any, **kwargs: Any) -> int: + return program.targetpopulation_set.count() + class CashPlanNode(BaseNodePermissionMixin, DjangoObjectType): permission_classes: Tuple[Type[BasePermission], ...] = ( diff --git a/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_copy_program.py b/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_copy_program.py index d908f6d7de..2ae6850fe0 100644 --- a/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_copy_program.py +++ b/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_copy_program.py @@ -321,7 +321,7 @@ 'Round 3C', 'Round 4D' ], - 'subtype': 'BOOLEAN' + 'subtype': 'BOOL' } } ], diff --git a/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_create_program.py b/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_create_program.py index db679a1eb6..368164cc92 100644 --- a/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_create_program.py +++ b/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_create_program.py @@ -469,7 +469,7 @@ 'Round 3C', 'Round 4D' ], - 'subtype': 'BOOLEAN' + 'subtype': 'BOOL' } } ], diff --git a/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_program_query.py b/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_program_query.py index 3ac42391a1..bd936c207b 100644 --- a/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_program_query.py +++ b/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_program_query.py @@ -57,11 +57,12 @@ 'Round A', 'Round B' ], - 'subtype': 'BOOLEAN' + 'subtype': 'BOOL' } } ], - 'status': 'ACTIVE' + 'status': 'ACTIVE', + 'targetPopulationsCount': 1 } } } diff --git a/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_update_program.py b/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_update_program.py index 65067a2ce1..d640a378c6 100644 --- a/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_update_program.py +++ b/backend/hct_mis_api/apps/program/tests/snapshots/snap_test_update_program.py @@ -857,7 +857,7 @@ 'pduFields': [ { 'label': '{"English(EN)": "PDU Field - New"}', - 'name': 'pdu_field_-_new', + 'name': 'pdu_field_new', 'pduData': { 'numberOfRounds': 4, 'roundsNames': [ @@ -866,31 +866,31 @@ 'Round 3C', 'Round 4D' ], - 'subtype': 'BOOLEAN' + 'subtype': 'BOOL' } }, { - 'label': '{"English(EN)": "PDU Field - Updated"}', - 'name': 'pdu_field_-_updated', + 'label': '{"English(EN)": "PDU Field To Be Preserved"}', + 'name': 'pdu_field_to_be_preserved', 'pduData': { - 'numberOfRounds': 3, + 'numberOfRounds': 1, 'roundsNames': [ - 'Round 1 Updated', - 'Round 2 Updated', - 'Round 3 Updated' + 'Round To Be Preserved' ], - 'subtype': 'BOOLEAN' + 'subtype': 'DATE' } }, { - 'label': '{"English(EN)": "PDU Field To Be Preserved"}', - 'name': 'pdu_field_to_be_preserved', + 'label': '{"English(EN)": "PDU Field - Updated"}', + 'name': 'pdu_field_updated', 'pduData': { - 'numberOfRounds': 1, + 'numberOfRounds': 3, 'roundsNames': [ - 'Round To Be Preserved' + 'Round 1 Updated', + 'Round 2 Updated', + 'Round 3 Updated' ], - 'subtype': 'DATE' + 'subtype': 'BOOL' } } ], @@ -907,7 +907,7 @@ 'pduFields': [ { 'label': '{"English(EN)": "PDU Field - New"}', - 'name': 'pdu_field_-_new', + 'name': 'pdu_field_new', 'pduData': { 'numberOfRounds': 4, 'roundsNames': [ @@ -916,31 +916,31 @@ 'Round 3C', 'Round 4D' ], - 'subtype': 'BOOLEAN' + 'subtype': 'BOOL' } }, { - 'label': '{"English(EN)": "PDU Field - Updated"}', - 'name': 'pdu_field_-_updated', + 'label': '{"English(EN)": "PDU Field To Be Preserved"}', + 'name': 'pdu_field_to_be_preserved', 'pduData': { - 'numberOfRounds': 3, + 'numberOfRounds': 1, 'roundsNames': [ - 'Round 1 Updated', - 'Round 2 Updated', - 'Round 3 Updated' + 'Round To Be Preserved' ], - 'subtype': 'BOOLEAN' + 'subtype': 'DATE' } }, { - 'label': '{"English(EN)": "PDU Field To Be Preserved"}', - 'name': 'pdu_field_to_be_preserved', + 'label': '{"English(EN)": "PDU Field - Updated"}', + 'name': 'pdu_field_updated', 'pduData': { - 'numberOfRounds': 1, + 'numberOfRounds': 3, 'roundsNames': [ - 'Round To Be Preserved' + 'Round 1 Updated', + 'Round 2 Updated', + 'Round 3 Updated' ], - 'subtype': 'DATE' + 'subtype': 'BOOL' } } ] @@ -993,19 +993,6 @@ } ], 'pduFields': [ - { - 'label': '{"English(EN)": "PDU Field - Updated"}', - 'name': 'pdu_field_-_updated', - 'pduData': { - 'numberOfRounds': 3, - 'roundsNames': [ - 'Round 1 Updated', - 'Round 2 Updated', - 'Round 3 Updated' - ], - 'subtype': 'BOOLEAN' - } - }, { 'label': '{"English(EN)": "PDU Field 1"}', 'name': 'pdu_field_1', @@ -1017,7 +1004,7 @@ 'Round 3C', 'Round 4D' ], - 'subtype': 'BOOLEAN' + 'subtype': 'BOOL' } }, { @@ -1030,6 +1017,19 @@ ], 'subtype': 'DATE' } + }, + { + 'label': '{"English(EN)": "PDU Field - Updated"}', + 'name': 'pdu_field_updated', + 'pduData': { + 'numberOfRounds': 3, + 'roundsNames': [ + 'Round 1 Updated', + 'Round 2 Updated', + 'Round 3 Updated' + ], + 'subtype': 'BOOL' + } } ], 'status': 'DRAFT' @@ -1063,20 +1063,6 @@ } ], 'pduFields': [ - { - 'label': '{"English(EN)": "PDU Field - New"}', - 'name': 'pdu_field_-_new', - 'pduData': { - 'numberOfRounds': 4, - 'roundsNames': [ - 'Round 1A', - 'Round 2B', - 'Round 3C', - 'Round 4D' - ], - 'subtype': 'BOOLEAN' - } - }, { 'label': '{"English(EN)": "PDU Field 1"}', 'name': 'pdu_field_1', @@ -1087,7 +1073,21 @@ 'Round 2 Updated', 'Round 3 Updated' ], - 'subtype': 'BOOLEAN' + 'subtype': 'BOOL' + } + }, + { + 'label': '{"English(EN)": "PDU Field - New"}', + 'name': 'pdu_field_new', + 'pduData': { + 'numberOfRounds': 4, + 'roundsNames': [ + 'Round 1A', + 'Round 2B', + 'Round 3C', + 'Round 4D' + ], + 'subtype': 'BOOL' } }, { @@ -1210,7 +1210,7 @@ 'line': 3 } ], - 'message': 'It is not possible to change the names of existing rounds for a Program with RDI', + 'message': 'It is not possible to change the names of existing rounds for a Program with RDI or TP', 'path': [ 'updateProgram' ] @@ -1230,7 +1230,7 @@ 'line': 3 } ], - 'message': 'It is not possible to decrease the number of rounds for a Program with RDI', + 'message': 'It is not possible to decrease the number of rounds for a Program with RDI or TP', 'path': [ 'updateProgram' ] diff --git a/backend/hct_mis_api/apps/program/tests/test_copy_program.py b/backend/hct_mis_api/apps/program/tests/test_copy_program.py index 0afc745aed..28d65295df 100644 --- a/backend/hct_mis_api/apps/program/tests/test_copy_program.py +++ b/backend/hct_mis_api/apps/program/tests/test_copy_program.py @@ -474,7 +474,7 @@ def test_copy_program_with_pdu_fields(self) -> None: { "label": "PDU Field 4", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 4, "roundsNames": ["Round 1A", "Round 2B", "Round 3C", "Round 4D"], }, diff --git a/backend/hct_mis_api/apps/program/tests/test_create_program.py b/backend/hct_mis_api/apps/program/tests/test_create_program.py index 03c214bd68..53b227fb37 100644 --- a/backend/hct_mis_api/apps/program/tests/test_create_program.py +++ b/backend/hct_mis_api/apps/program/tests/test_create_program.py @@ -414,7 +414,7 @@ def test_create_program_with_pdu_fields(self) -> None: { "label": "PDU Field 4", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 4, "roundsNames": ["Round 1A", "Round 2B", "Round 3C", "Round 4D"], }, diff --git a/backend/hct_mis_api/apps/program/tests/test_program_query.py b/backend/hct_mis_api/apps/program/tests/test_program_query.py index 55b3bcb1ca..f933be0562 100644 --- a/backend/hct_mis_api/apps/program/tests/test_program_query.py +++ b/backend/hct_mis_api/apps/program/tests/test_program_query.py @@ -13,12 +13,14 @@ from hct_mis_api.apps.core.models import PeriodicFieldData from hct_mis_api.apps.program.fixtures import ProgramFactory from hct_mis_api.apps.program.models import Program +from hct_mis_api.apps.targeting.fixtures import TargetPopulationFactory PROGRAM_QUERY = """ query Program($id: ID!) { program(id: $id) { name status + targetPopulationsCount pduFields { label name @@ -72,7 +74,7 @@ def setUpTestData(cls) -> None: pdu_data=pdu_data3, ) pdu_data4 = PeriodicFieldDataFactory( - subtype=PeriodicFieldData.BOOLEAN, + subtype=PeriodicFieldData.BOOL, number_of_rounds=2, rounds_names=["Round A", "Round B"], ) @@ -95,6 +97,7 @@ def setUpTestData(cls) -> None: label="PDU Field Other", pdu_data=pdu_data_other, ) + TargetPopulationFactory(program=cls.program) @parameterized.expand( [ @@ -103,6 +106,7 @@ def setUpTestData(cls) -> None: ] ) def test_single_program_query(self, _: Any, permissions: List[Permissions]) -> None: + print(self.program.targetpopulation_set.all()) self.create_user_role_with_permissions(self.user, permissions, self.business_area) self.snapshot_graphql_request( request_string=PROGRAM_QUERY, diff --git a/backend/hct_mis_api/apps/program/tests/test_update_program.py b/backend/hct_mis_api/apps/program/tests/test_update_program.py index 3b9828dfe7..56d012e189 100644 --- a/backend/hct_mis_api/apps/program/tests/test_update_program.py +++ b/backend/hct_mis_api/apps/program/tests/test_update_program.py @@ -651,7 +651,7 @@ def test_update_program_with_pdu_fields(self) -> None: "id": self.id_to_base64(self.pdu_field_to_be_updated.id, "PeriodicFieldNode"), "label": "PDU Field - Updated", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 3, "roundsNames": ["Round 1 Updated", "Round 2 Updated", "Round 3 Updated"], }, @@ -659,7 +659,7 @@ def test_update_program_with_pdu_fields(self) -> None: { "label": "PDU Field - New", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 4, "roundsNames": ["Round 1A", "Round 2B", "Round 3C", "Round 4D"], }, @@ -693,10 +693,8 @@ def test_update_program_with_pdu_fields(self) -> None: ) self.assertIsNone(FlexibleAttribute.objects.filter(name="pdu_field_to_be_removed").first()) self.assertIsNone(FlexibleAttribute.objects.filter(name="pdu_field_to_be_updated").first()) - self.assertEqual( - FlexibleAttribute.objects.filter(name="pdu_field_-_updated").first().pdu_data.subtype, "BOOLEAN" - ) - self.assertIsNotNone(FlexibleAttribute.objects.filter(name="pdu_field_-_new").first()) + self.assertEqual(FlexibleAttribute.objects.filter(name="pdu_field_updated").first().pdu_data.subtype, "BOOL") + self.assertIsNotNone(FlexibleAttribute.objects.filter(name="pdu_field_new").first()) self.assertIsNotNone(FlexibleAttribute.objects.filter(name="pdu_field_to_be_preserved").first()) def test_update_program_with_pdu_fields_invalid_data(self) -> None: @@ -719,7 +717,7 @@ def test_update_program_with_pdu_fields_invalid_data(self) -> None: "id": self.id_to_base64(self.pdu_field_to_be_updated.id, "PeriodicFieldNode"), "label": "PDU Field - Updated", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 1, "roundsNames": ["Round 1 Updated", "Round 2 Updated", "Round 3 Updated"], }, @@ -727,7 +725,7 @@ def test_update_program_with_pdu_fields_invalid_data(self) -> None: { "label": "PDU Field - New", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 3, "roundsNames": ["Round 1A", "Round 2B", "Round 3C", "Round 4D"], }, @@ -765,7 +763,7 @@ def test_update_program_with_pdu_fields_duplicated_field_names_in_input(self) -> "id": self.id_to_base64(self.pdu_field_to_be_updated.id, "PeriodicFieldNode"), "label": "PDU Field 1", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 3, "roundsNames": ["Round 1 Updated", "Round 2 Updated", "Round 3 Updated"], }, @@ -773,7 +771,7 @@ def test_update_program_with_pdu_fields_duplicated_field_names_in_input(self) -> { "label": "PDU Field 1", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 4, "roundsNames": ["Round 1A", "Round 2B", "Round 3C", "Round 4D"], }, @@ -822,7 +820,7 @@ def test_update_program_with_pdu_fields_existing_field_name_for_new_field(self) "id": self.id_to_base64(self.pdu_field_to_be_updated.id, "PeriodicFieldNode"), "label": "PDU Field - Updated", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 3, "roundsNames": ["Round 1 Updated", "Round 2 Updated", "Round 3 Updated"], }, @@ -830,7 +828,7 @@ def test_update_program_with_pdu_fields_existing_field_name_for_new_field(self) { "label": "PDU Field 1", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 4, "roundsNames": ["Round 1A", "Round 2B", "Round 3C", "Round 4D"], }, @@ -879,7 +877,7 @@ def test_update_program_with_pdu_fields_existing_field_name_for_updated_field(se "id": self.id_to_base64(self.pdu_field_to_be_updated.id, "PeriodicFieldNode"), "label": "PDU Field 1", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 3, "roundsNames": ["Round 1 Updated", "Round 2 Updated", "Round 3 Updated"], }, @@ -887,7 +885,7 @@ def test_update_program_with_pdu_fields_existing_field_name_for_updated_field(se { "label": "PDU Field - New", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 4, "roundsNames": ["Round 1A", "Round 2B", "Round 3C", "Round 4D"], }, @@ -916,7 +914,7 @@ def test_update_program_with_pdu_fields_program_has_RDI(self) -> None: "id": self.id_to_base64(self.pdu_field_to_be_updated.id, "PeriodicFieldNode"), "label": "PDU Field - NAME WILL NOT BE UPDATED", "pduData": { - "subtype": "BOOLEAN", # subtype will NOT be updated + "subtype": "BOOL", # subtype will NOT be updated "numberOfRounds": 4, "roundsNames": [ "Round 1 To Be Updated", @@ -950,7 +948,7 @@ def test_update_program_with_pdu_fields_program_has_RDI_new_field(self) -> None: { "label": "PDU Field - New", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 4, "roundsNames": ["Round 1A", "Round 2B", "Round 3C", "Round 4D"], }, @@ -980,7 +978,7 @@ def test_update_program_with_pdu_fields_program_has_RDI_update_pdu_field(self) - "id": self.id_to_base64(self.pdu_field_to_be_updated.id, "PeriodicFieldNode"), "label": "PDU Field - Updated", "pduData": { - "subtype": "BOOLEAN", + "subtype": "BOOL", "numberOfRounds": 2, "roundsNames": ["Round 1 To Be Updated", "Round 2 To Be Updated"], }, diff --git a/backend/hct_mis_api/apps/program/utils.py b/backend/hct_mis_api/apps/program/utils.py index 9ff220c709..ff46959c96 100644 --- a/backend/hct_mis_api/apps/program/utils.py +++ b/backend/hct_mis_api/apps/program/utils.py @@ -467,8 +467,8 @@ def copy_individual(individual: Individual, program: Program) -> tuple: original_individual_id = individual.id individual.copied_from_id = original_individual_id individual.pk = None - copied_flex_fields = get_flex_fields_without_pdu_values(individual) - individual.flex_fields = populate_pdu_with_null_values(program, copied_flex_fields) + individual.flex_fields = get_flex_fields_without_pdu_values(individual) + populate_pdu_with_null_values(program, individual.flex_fields) individual.program = program individual.household = None individual.registration_data_import = None diff --git a/backend/hct_mis_api/apps/registration_datahub/tasks/rdi_xlsx_create.py b/backend/hct_mis_api/apps/registration_datahub/tasks/rdi_xlsx_create.py index ffeb9f353e..bd1d0f59de 100644 --- a/backend/hct_mis_api/apps/registration_datahub/tasks/rdi_xlsx_create.py +++ b/backend/hct_mis_api/apps/registration_datahub/tasks/rdi_xlsx_create.py @@ -519,7 +519,7 @@ def handle_pdu_fields(self, row: list[Any], header: list[Any], individual: Pendi PeriodicFieldData.DATE: self._handle_date_field, PeriodicFieldData.DECIMAL: self._handle_decimal_field, PeriodicFieldData.STRING: self._handle_string_field, - PeriodicFieldData.BOOLEAN: self._handle_bool_field, + PeriodicFieldData.BOOL: self._handle_bool_field, } for flexible_attribute in self.pdu_flexible_attributes: column_value = f"{flexible_attribute.name}_round_1_value" diff --git a/backend/hct_mis_api/apps/registration_datahub/tests/test_xlsx_upload_validators_methods.py b/backend/hct_mis_api/apps/registration_datahub/tests/test_xlsx_upload_validators_methods.py index d8025a8269..ec1be97383 100644 --- a/backend/hct_mis_api/apps/registration_datahub/tests/test_xlsx_upload_validators_methods.py +++ b/backend/hct_mis_api/apps/registration_datahub/tests/test_xlsx_upload_validators_methods.py @@ -777,7 +777,7 @@ def test_validate_delivery_mechanism_data_global_fields_only_dropped(self) -> No [ (PeriodicFieldData.STRING, ["Test", "2021-05-01"]), (PeriodicFieldData.DECIMAL, ["12.3", "2021-05-01"]), - (PeriodicFieldData.BOOLEAN, ["True", "2021-05-01"]), + (PeriodicFieldData.BOOL, ["True", "2021-05-01"]), (PeriodicFieldData.DATE, ["1996-06-21", "2021-05-01"]), ] ) @@ -801,7 +801,7 @@ def test_validate_pdu_string_valid(self, subtype: str, data_row: list) -> None: @parameterized.expand( [ (PeriodicFieldData.DECIMAL, ["foo", "2021-05-01"]), - (PeriodicFieldData.BOOLEAN, ["foo", "2021-05-01"]), + (PeriodicFieldData.BOOL, ["foo", "2021-05-01"]), (PeriodicFieldData.DATE, ["foo", "2021-05-01"]), ] ) diff --git a/backend/hct_mis_api/apps/registration_datahub/validators.py b/backend/hct_mis_api/apps/registration_datahub/validators.py index e584fba7f3..85e3d44366 100644 --- a/backend/hct_mis_api/apps/registration_datahub/validators.py +++ b/backend/hct_mis_api/apps/registration_datahub/validators.py @@ -1179,7 +1179,7 @@ def _validate_pdu(self, row: list[Any], header_row: list[Any], row_number: int) PeriodicFieldData.DATE: self.date_validator, PeriodicFieldData.DECIMAL: self.decimal_validator, PeriodicFieldData.STRING: self.string_validator, - PeriodicFieldData.BOOLEAN: self.bool_validator, + PeriodicFieldData.BOOL: self.bool_validator, } errors = [] for flexible_attribute in self.pdu_flexible_attributes: diff --git a/backend/hct_mis_api/apps/targeting/choices.py b/backend/hct_mis_api/apps/targeting/choices.py new file mode 100644 index 0000000000..213f617579 --- /dev/null +++ b/backend/hct_mis_api/apps/targeting/choices.py @@ -0,0 +1,8 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class FlexFieldClassification(models.TextChoices): + NOT_FLEX_FIELD = "NOT_FLEX_FIELD", _("Not Flex Field") + FLEX_FIELD_BASIC = "FLEX_FIELD_BASIC", _("Flex Field Basic") + FLEX_FIELD_PDU = "FLEX_FIELD_PDU", _("Flex Field PDU") diff --git a/backend/hct_mis_api/apps/targeting/fixtures/data-cypress.json b/backend/hct_mis_api/apps/targeting/fixtures/data-cypress.json index 1ccdcab6ad..aaafd3e917 100644 --- a/backend/hct_mis_api/apps/targeting/fixtures/data-cypress.json +++ b/backend/hct_mis_api/apps/targeting/fixtures/data-cypress.json @@ -163,7 +163,7 @@ "updated_at": "2023-06-02T10:00:40.202Z", "comparison_method": "RANGE", "targeting_criteria_rule": "5c632b70-1b2a-4eb2-957b-2f1b20d77228", - "is_flex_field": false, + "flex_field_classification": "NOT_FLEX_FIELD", "field_name": "size", "arguments": [1, 11] } @@ -176,7 +176,7 @@ "updated_at": "2023-06-02T10:00:08.547Z", "comparison_method": "RANGE", "targeting_criteria_rule": "f99cd30f-c986-47b9-84ee-bc5ed1c1378c", - "is_flex_field": false, + "flex_field_classification": "NOT_FLEX_FIELD", "field_name": "size", "arguments": [1, 11] } diff --git a/backend/hct_mis_api/apps/targeting/graphql_types.py b/backend/hct_mis_api/apps/targeting/graphql_types.py index a1839d4b89..01641ee9f2 100644 --- a/backend/hct_mis_api/apps/targeting/graphql_types.py +++ b/backend/hct_mis_api/apps/targeting/graphql_types.py @@ -18,7 +18,10 @@ from hct_mis_api.apps.core.field_attributes.fields_types import Scope from hct_mis_api.apps.core.models import FlexibleAttribute from hct_mis_api.apps.core.schema import ExtendedConnection, FieldAttributeNode +from hct_mis_api.apps.core.utils import decode_id_string from hct_mis_api.apps.household.schema import HouseholdNode +from hct_mis_api.apps.program.models import Program +from hct_mis_api.apps.targeting.choices import FlexFieldClassification from hct_mis_api.apps.targeting.filters import HouseholdFilter, TargetPopulationFilter from hct_mis_api.apps.utils.schema import Arg @@ -53,6 +56,12 @@ def filter_choices(field: Optional[Dict], args: List) -> Optional[Dict]: return field +class FlexFieldClassificationChoices(graphene.Enum): + NOT_FLEX_FIELD = "NOT_FLEX_FIELD" + FLEX_FIELD_BASIC = "FLEX_FIELD_BASIC" + FLEX_FIELD_PDU = "FLEX_FIELD_PDU" + + class TargetingCriteriaRuleFilterNode(DjangoObjectType): arguments = graphene.List(Arg) field_attribute = graphene.Field(FieldAttributeNode) @@ -61,17 +70,17 @@ def resolve_arguments(self, info: Any) -> "GrapheneList": return self.arguments def resolve_field_attribute(parent, info: Any) -> Optional[Dict]: - if parent.is_flex_field: - return FlexibleAttribute.objects.get(name=parent.field_name) - else: + if parent.flex_field_classification == FlexFieldClassification.NOT_FLEX_FIELD: field_attribute = get_field_by_name( parent.field_name, parent.targeting_criteria_rule.targeting_criteria.target_population ) - parent.targeting_criteria_rule return filter_choices( field_attribute, parent.arguments # type: ignore # can't convert graphene list to list ) + else: # FlexFieldClassification.FLEX_FIELD_BASIC + return FlexibleAttribute.objects.get(name=parent.field_name) + class Meta: model = target_models.TargetingCriteriaRuleFilter @@ -84,14 +93,18 @@ def resolve_arguments(self, info: Any) -> "GrapheneList": return self.arguments def resolve_field_attribute(parent, info: Any) -> Any: - if parent.is_flex_field: - return FlexibleAttribute.objects.get(name=parent.field_name) + if parent.flex_field_classification == FlexFieldClassification.NOT_FLEX_FIELD: + field_attribute = get_field_by_name( + parent.field_name, + parent.individuals_filters_block.targeting_criteria_rule.targeting_criteria.target_population, + ) + return filter_choices(field_attribute, parent.arguments) # type: ignore # can't convert graphene list to list - field_attribute = get_field_by_name( - parent.field_name, - parent.individuals_filters_block.targeting_criteria_rule.targeting_criteria.target_population, - ) - return filter_choices(field_attribute, parent.arguments) # type: ignore # can't convert graphene list to list + program = None + if parent.flex_field_classification == FlexFieldClassification.FLEX_FIELD_PDU: + encoded_program_id = info.context.headers.get("Program") + program = Program.objects.get(id=decode_id_string(encoded_program_id)) + return FlexibleAttribute.objects.get(name=parent.field_name, program=program) class Meta: model = target_models.TargetingIndividualBlockRuleFilter @@ -163,9 +176,10 @@ class Meta: class TargetingCriteriaRuleFilterObjectType(graphene.InputObjectType): comparison_method = graphene.String(required=True) - is_flex_field = graphene.Boolean(required=True) + flex_field_classification = graphene.Field(FlexFieldClassificationChoices, required=True) field_name = graphene.String(required=True) arguments = graphene.List(Arg, required=True) + round_number = graphene.Int() class TargetingIndividualRuleFilterBlockObjectType(graphene.InputObjectType): diff --git a/backend/hct_mis_api/apps/targeting/migrations/0046_migration.py b/backend/hct_mis_api/apps/targeting/migrations/0046_migration.py new file mode 100644 index 0000000000..f87649645d --- /dev/null +++ b/backend/hct_mis_api/apps/targeting/migrations/0046_migration.py @@ -0,0 +1,67 @@ +# Generated by Django 3.2.25 on 2024-08-05 23:07 + +from django.db import migrations, models + + +def convert_is_flex_field_to_classification(apps, schema_editor): + TargetingCriteriaRuleFilter = apps.get_model('targeting', 'TargetingCriteriaRuleFilter') + TargetingCriteriaRuleFilter.objects.filter(is_flex_field=True).update(flex_field_classification='FLEX_FIELD_BASIC') + TargetingCriteriaRuleFilter.objects.filter(is_flex_field=False).update(flex_field_classification='NOT_FLEX_FIELD') + + TargetingIndividualBlockRuleFilter = apps.get_model('targeting', 'TargetingIndividualBlockRuleFilter') + TargetingIndividualBlockRuleFilter.objects.filter(is_flex_field=True).update(flex_field_classification='FLEX_FIELD_BASIC') + TargetingIndividualBlockRuleFilter.objects.filter(is_flex_field=False).update(flex_field_classification='NOT_FLEX_FIELD') + + +class Migration(migrations.Migration): + + dependencies = [ + ('targeting', '0045_migration'), + ] + + operations = [ + migrations.AddField( + model_name='targetingcriteriarulefilter', + name='flex_field_classification', + field=models.CharField(choices=[('NOT_FLEX_FIELD', 'Not Flex Field'), ('FLEX_FIELD_BASIC', 'Flex Field Basic'), ('FLEX_FIELD_PDU', 'Flex Field PDU')], default='NOT_FLEX_FIELD', max_length=20), + ), + migrations.AddField( + model_name='targetingindividualblockrulefilter', + name='flex_field_classification', + field=models.CharField(choices=[('NOT_FLEX_FIELD', 'Not Flex Field'), ('FLEX_FIELD_BASIC', 'Flex Field Basic'), ('FLEX_FIELD_PDU', 'Flex Field PDU')], default='NOT_FLEX_FIELD', max_length=20), + ), + migrations.RunPython(convert_is_flex_field_to_classification), + migrations.RemoveField( + model_name='targetingcriteriarulefilter', + name='is_flex_field', + ), + migrations.RemoveField( + model_name='targetingindividualblockrulefilter', + name='is_flex_field', + ), + migrations.AddField( + model_name='targetingindividualblockrulefilter', + name='round_number', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='targetingcriteriarulefilter', + name='comparison_method', + field=models.CharField( + choices=[('EQUALS', 'Equals'), ('NOT_EQUALS', 'Not Equals'), ('CONTAINS', 'Contains'), + ('NOT_CONTAINS', 'Does not contain'), ('RANGE', 'In between <>'), + ('NOT_IN_RANGE', 'Not in between <>'), ('GREATER_THAN', 'Greater than'), + ('LESS_THAN', 'Less than'), ('IS_NULL', 'Is null')], max_length=20), + ), + migrations.AlterField( + model_name='targetingindividualblockrulefilter', + name='comparison_method', + field=models.CharField( + choices=[('EQUALS', 'Equals'), ('NOT_EQUALS', 'Not Equals'), ('CONTAINS', 'Contains'), + ('NOT_CONTAINS', 'Does not contain'), ('RANGE', 'In between <>'), + ('NOT_IN_RANGE', 'Not in between <>'), ('GREATER_THAN', 'Greater than'), + ('LESS_THAN', 'Less than'), ('IS_NULL', 'Is null')], max_length=20), + ), + ] + + diff --git a/backend/hct_mis_api/apps/targeting/models.py b/backend/hct_mis_api/apps/targeting/models.py index 98c9aade33..f743bbc962 100644 --- a/backend/hct_mis_api/apps/targeting/models.py +++ b/backend/hct_mis_api/apps/targeting/models.py @@ -23,6 +23,7 @@ from hct_mis_api.apps.core.utils import map_unicef_ids_to_households_unicef_ids from hct_mis_api.apps.household.models import Household from hct_mis_api.apps.steficon.models import Rule, RuleCommit +from hct_mis_api.apps.targeting.choices import FlexFieldClassification from hct_mis_api.apps.targeting.services.targeting_service import ( TargetingCriteriaFilterBase, TargetingCriteriaQueryingBase, @@ -448,21 +449,6 @@ class TargetingCriteriaRuleFilter(TimeStampedUUIDModel, TargetingCriteriaFilterB :Residential Status != Refugee """ - @property - def is_social_worker_program(self) -> bool: - try: - return self.targeting_criteria_rule.targeting_criteria.target_population.program.is_social_worker_program - except ( - AttributeError, - TargetingCriteriaRuleFilter.targeting_criteria_rule.RelatedObjectDoesNotExist, - ): - return False - - def get_core_fields(self) -> List: - if self.is_social_worker_program: - return FieldFactory.from_only_scopes([Scope.TARGETING, Scope.XLSX_PEOPLE]) - return FieldFactory.from_scope(Scope.TARGETING).associated_with_household() - comparison_method = models.CharField( max_length=20, choices=TargetingCriteriaFilterBase.COMPARISON_CHOICES, @@ -472,7 +458,11 @@ def get_core_fields(self) -> List: related_name="filters", on_delete=models.CASCADE, ) - is_flex_field = models.BooleanField(default=False) + flex_field_classification = models.CharField( + max_length=20, + choices=FlexFieldClassification.choices, + default=FlexFieldClassification.NOT_FLEX_FIELD, + ) field_name = models.CharField(max_length=50) arguments = JSONField( help_text=""" @@ -480,6 +470,21 @@ def get_core_fields(self) -> List: """ ) + @property + def is_social_worker_program(self) -> bool: + try: + return self.targeting_criteria_rule.targeting_criteria.target_population.program.is_social_worker_program + except ( + AttributeError, + TargetingCriteriaRuleFilter.targeting_criteria_rule.RelatedObjectDoesNotExist, + ): + return False + + def get_core_fields(self) -> List: + if self.is_social_worker_program: + return FieldFactory.from_only_scopes([Scope.TARGETING, Scope.XLSX_PEOPLE]) + return FieldFactory.from_scope(Scope.TARGETING).associated_with_household() + class TargetingIndividualBlockRuleFilter(TimeStampedUUIDModel, TargetingCriteriaFilterBase): """ @@ -489,13 +494,6 @@ class TargetingIndividualBlockRuleFilter(TimeStampedUUIDModel, TargetingCriteria :Residential Status != Refugee """ - @property - def is_social_worker_program(self) -> bool: - return False - - def get_core_fields(self) -> List: - return FieldFactory.from_scope(Scope.TARGETING).associated_with_individual() - comparison_method = models.CharField( max_length=20, choices=TargetingCriteriaFilterBase.COMPARISON_CHOICES, @@ -505,13 +503,25 @@ def get_core_fields(self) -> List: related_name="individual_block_filters", on_delete=models.CASCADE, ) - is_flex_field = models.BooleanField(default=False) + flex_field_classification = models.CharField( + max_length=20, + choices=FlexFieldClassification.choices, + default=FlexFieldClassification.NOT_FLEX_FIELD, + ) field_name = models.CharField(max_length=50) arguments = JSONField( help_text=""" Array of arguments """ ) + round_number = models.PositiveIntegerField(null=True, blank=True) + + @property + def is_social_worker_program(self) -> bool: + return False + + def get_core_fields(self) -> List: + return FieldFactory.from_scope(Scope.TARGETING).associated_with_individual() def get_lookup_prefix(self, associated_with: Any) -> str: return "" diff --git a/backend/hct_mis_api/apps/targeting/services/targeting_service.py b/backend/hct_mis_api/apps/targeting/services/targeting_service.py index 3c46288270..84e810bbb8 100644 --- a/backend/hct_mis_api/apps/targeting/services/targeting_service.py +++ b/backend/hct_mis_api/apps/targeting/services/targeting_service.py @@ -19,6 +19,7 @@ from hct_mis_api.apps.core.utils import get_attr_value from hct_mis_api.apps.grievance.models import GrievanceTicket from hct_mis_api.apps.household.models import Household, Individual +from hct_mis_api.apps.targeting.choices import FlexFieldClassification logger = logging.getLogger(__name__) @@ -243,6 +244,12 @@ class TargetingCriteriaFilterBase: "negative": False, "supported_types": ["INTEGER", "DECIMAL", "DATE"], }, + "IS_NULL": { + "arguments": 1, + "lookup": "", + "negative": False, + "supported_types": ["DECIMAL", "DATE", "STRING", "BOOL"], + }, } COMPARISON_CHOICES = Choices( @@ -254,10 +261,15 @@ class TargetingCriteriaFilterBase: ("NOT_IN_RANGE", _("Not in between <>")), ("GREATER_THAN", _("Greater than")), ("LESS_THAN", _("Less than")), + ("IS_NULL", _("Is null")), ) + @property + def field_name_combined(self) -> str: + return f"{self.field_name}__{self.round_number}" if self.round_number else self.field_name + def get_criteria_string(self) -> str: - return f"{{{self.field_name} {self.comparison_method} ({','.join([str(x) for x in self.arguments])})}}" + return f"{{{self.field_name_combined} {self.comparison_method} ({','.join([str(x) for x in self.arguments])})}}" def get_lookup_prefix(self, associated_with: str) -> str: return "individuals__" if associated_with == _INDIVIDUAL else "" @@ -267,6 +279,10 @@ def prepare_arguments(self, arguments: List, field_attr: str) -> List: if not is_flex_field: return arguments type = get_attr_value("type", field_attr, None) + if type == FlexibleAttribute.PDU: + if arguments == [None]: + return arguments + type = field_attr.pdu_data.subtype if type == TYPE_DECIMAL: return [float(arg) for arg in arguments] if type == TYPE_INTEGER: @@ -316,6 +332,13 @@ def get_query_for_lookup( query = Q(**{f"{lookup}{comparison_attribute.get('lookup')}": argument}) if comparison_attribute.get("negative"): return ~query + # ignore null values for PDU flex fields + if ( + self.comparison_method != "IS_NULL" + and self.flex_field_classification == FlexFieldClassification.FLEX_FIELD_PDU + ): + query &= ~Q(**{f"{lookup}": None}) + return query def get_query_for_core_field(self) -> Q: @@ -346,18 +369,44 @@ def get_query_for_core_field(self) -> Q: return self.get_query_for_lookup(f"{lookup_prefix}{lookup}", core_field_attr) def get_query_for_flex_field(self) -> Q: - flex_field_attr = FlexibleAttribute.objects.get(name=self.field_name) - if not flex_field_attr: - logger.error(f"There are no Flex Field Attributes associated with this fieldName {self.field_name}") - raise ValidationError( - f"There are no Flex Field Attributes associated with this fieldName {self.field_name}" + if self.flex_field_classification == FlexFieldClassification.FLEX_FIELD_PDU: + program = ( + self.individuals_filters_block.targeting_criteria_rule.targeting_criteria.target_population.program ) + flex_field_attr = FlexibleAttribute.objects.filter(name=self.field_name, program=program).first() + if not flex_field_attr: + logger.error( + f"There is no PDU Flex Field Attribute associated with this fieldName {self.field_name} in program {program.name}" + ) + raise ValidationError( + f"There is no PDU Flex Field Attribute associated with this fieldName {self.field_name} in program {program.name}" + ) + if not self.round_number: + logger.error(f"Round number is missing for PDU Flex Field Attribute {self.field_name}") + raise ValidationError(f"Round number is missing for PDU Flex Field Attribute {self.field_name}") + flex_field_attr_rounds_number = flex_field_attr.pdu_data.number_of_rounds + if self.round_number > flex_field_attr_rounds_number: + logger.error( + f"Round number {self.round_number} is greater than the number of rounds for PDU Flex Field Attribute {self.field_name}" + ) + raise ValidationError( + f"Round number {self.round_number} is greater than the number of rounds for PDU Flex Field Attribute {self.field_name}" + ) + field_name_combined = f"{flex_field_attr.name}__{self.round_number}__value" + else: + flex_field_attr = FlexibleAttribute.objects.filter(name=self.field_name, program=None).first() + if not flex_field_attr: + logger.error(f"There is no Flex Field Attributes associated with this fieldName {self.field_name}") + raise ValidationError( + f"There is no Flex Field Attributes associated with this fieldName {self.field_name}" + ) + field_name_combined = flex_field_attr.name lookup_prefix = self.get_lookup_prefix(_INDIVIDUAL if flex_field_attr.associated_with == 1 else _HOUSEHOLD) - lookup = f"{lookup_prefix}flex_fields__{flex_field_attr.name}" + lookup = f"{lookup_prefix}flex_fields__{field_name_combined}" return self.get_query_for_lookup(lookup, flex_field_attr) def get_query(self) -> Q: - if not self.is_flex_field: + if self.flex_field_classification == FlexFieldClassification.NOT_FLEX_FIELD: return self.get_query_for_core_field() return self.get_query_for_flex_field() diff --git a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_copy_target_population_mutation.py b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_copy_target_population_mutation.py index 5e88775e76..08f55cdc7d 100644 --- a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_copy_target_population_mutation.py +++ b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_copy_target_population_mutation.py @@ -57,7 +57,7 @@ ], 'comparisonMethod': 'EQUALS', 'fieldName': 'size', - 'isFlexField': False + 'flexFieldClassification': 'NOT_FLEX_FIELD' } ] } diff --git a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_create_target_population_mutation.py b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_create_target_population_mutation.py index a646512360..700e1db720 100644 --- a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_create_target_population_mutation.py +++ b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_create_target_population_mutation.py @@ -30,8 +30,10 @@ ], 'comparisonMethod': 'EQUALS', 'fieldName': 'size', - 'isFlexField': False + 'flexFieldClassification': 'NOT_FLEX_FIELD' } + ], + 'individualsFiltersBlocks': [ ] } ] @@ -306,3 +308,90 @@ } ] } + +snapshots['TestCreateTargetPopulationMutation::test_create_mutation_with_flex_field 1'] = { + 'data': { + 'createTargetPopulation': { + 'targetPopulation': { + 'hasEmptyCriteria': False, + 'hasEmptyIdsCriteria': True, + 'name': 'Example name 5', + 'programCycle': { + 'status': 'ACTIVE' + }, + 'status': 'OPEN', + 'targetingCriteria': { + 'householdIds': '', + 'individualIds': '', + 'rules': [ + { + 'filters': [ + ], + 'individualsFiltersBlocks': [ + { + 'individualBlockFilters': [ + { + 'arguments': [ + 'Average' + ], + 'comparisonMethod': 'CONTAINS', + 'fieldName': 'flex_field_1', + 'flexFieldClassification': 'FLEX_FIELD_BASIC', + 'roundNumber': None + } + ] + } + ] + } + ] + }, + 'totalHouseholdsCount': None, + 'totalIndividualsCount': None + } + } + } +} + +snapshots['TestCreateTargetPopulationMutation::test_create_mutation_with_pdu_flex_field 1'] = { + 'data': { + 'createTargetPopulation': { + 'targetPopulation': { + 'hasEmptyCriteria': False, + 'hasEmptyIdsCriteria': True, + 'name': 'Example name 5', + 'programCycle': { + 'status': 'ACTIVE' + }, + 'status': 'OPEN', + 'targetingCriteria': { + 'householdIds': '', + 'individualIds': '', + 'rules': [ + { + 'filters': [ + ], + 'individualsFiltersBlocks': [ + { + 'individualBlockFilters': [ + { + 'arguments': [ + '2', + '3.5' + ], + 'comparisonMethod': 'RANGE', + 'fieldName': 'pdu_field_1', + 'flexFieldClassification': 'FLEX_FIELD_PDU', + 'roundNumber': 1 + } + ] + } + ] + } + ] + }, + 'totalHouseholdsCount': None, + 'totalIndividualsCount': None + } + } + } +} diff --git a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_target_query.py b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_target_query.py index 66dc759a26..300dc641e2 100644 --- a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_target_query.py +++ b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_target_query.py @@ -63,6 +63,30 @@ 'totalHouseholdsCount': 1, 'totalIndividualsCount': 2 } + }, + { + 'node': { + 'createdBy': { + 'firstName': 'Third', + 'lastName': 'User' + }, + 'name': 'target_population_with_pdu_filter', + 'status': 'LOCKED', + 'totalHouseholdsCount': 1, + 'totalIndividualsCount': 3 + } + }, + { + 'node': { + 'createdBy': { + 'firstName': 'Third', + 'lastName': 'User' + }, + 'name': 'target_population_with_individual_filter', + 'status': 'LOCKED', + 'totalHouseholdsCount': 1, + 'totalIndividualsCount': 3 + } } ] } @@ -96,6 +120,22 @@ 'totalHouseholdsCount': 2, 'totalIndividualsCount': 2 } + }, + { + 'node': { + 'name': 'target_population_with_pdu_filter', + 'status': 'LOCKED', + 'totalHouseholdsCount': 1, + 'totalIndividualsCount': 3 + } + }, + { + 'node': { + 'name': 'target_population_with_individual_filter', + 'status': 'LOCKED', + 'totalHouseholdsCount': 1, + 'totalIndividualsCount': 3 + } } ] } @@ -149,6 +189,22 @@ 'totalHouseholdsCount': 2, 'totalIndividualsCount': 2 } + }, + { + 'node': { + 'name': 'target_population_with_pdu_filter', + 'status': 'LOCKED', + 'totalHouseholdsCount': 1, + 'totalIndividualsCount': 3 + } + }, + { + 'node': { + 'name': 'target_population_with_individual_filter', + 'status': 'LOCKED', + 'totalHouseholdsCount': 1, + 'totalIndividualsCount': 3 + } } ] } @@ -176,8 +232,10 @@ 'type': 'INTEGER' }, 'fieldName': 'size', - 'isFlexField': False + 'flexFieldClassification': 'NOT_FLEX_FIELD' } + ], + 'individualsFiltersBlocks': [ ] } ] @@ -208,6 +266,66 @@ ] } +snapshots['TestTargetPopulationQuery::test_simple_target_query_individual_filter_0_with_permission 1'] = { + 'data': { + 'targetPopulation': { + 'hasEmptyCriteria': False, + 'hasEmptyIdsCriteria': True, + 'name': 'target_population_with_individual_filter', + 'status': 'LOCKED', + 'targetingCriteria': { + 'rules': [ + { + 'filters': [ + ], + 'individualsFiltersBlocks': [ + { + 'individualBlockFilters': [ + { + 'arguments': [ + 'disabled' + ], + 'comparisonMethod': 'EQUALS', + 'fieldAttribute': { + 'labelEn': 'Individual is disabled?', + 'type': 'SELECT_ONE' + }, + 'fieldName': 'disability', + 'flexFieldClassification': 'NOT_FLEX_FIELD', + 'roundNumber': None + } + ] + } + ] + } + ] + }, + 'totalHouseholdsCount': 1, + 'totalIndividualsCount': 3 + } + } +} + +snapshots['TestTargetPopulationQuery::test_simple_target_query_individual_filter_1_without_permission 1'] = { + 'data': { + 'targetPopulation': None + }, + 'errors': [ + { + 'locations': [ + { + 'column': 11, + 'line': 3 + } + ], + 'message': 'Permission Denied', + 'path': [ + 'targetPopulation' + ] + } + ] +} + snapshots['TestTargetPopulationQuery::test_simple_target_query_next_0_with_permission 1'] = { 'data': { 'targetPopulation': { @@ -229,8 +347,10 @@ 'type': 'SELECT_ONE' }, 'fieldName': 'residence_status', - 'isFlexField': False + 'flexFieldClassification': 'NOT_FLEX_FIELD' } + ], + 'individualsFiltersBlocks': [ ] } ] @@ -260,3 +380,63 @@ } ] } + +snapshots['TestTargetPopulationQuery::test_simple_target_query_pdu_0_with_permission 1'] = { + 'data': { + 'targetPopulation': { + 'hasEmptyCriteria': False, + 'hasEmptyIdsCriteria': True, + 'name': 'target_population_with_pdu_filter', + 'status': 'LOCKED', + 'targetingCriteria': { + 'rules': [ + { + 'filters': [ + ], + 'individualsFiltersBlocks': [ + { + 'individualBlockFilters': [ + { + 'arguments': [ + 'some' + ], + 'comparisonMethod': 'EQUALS', + 'fieldAttribute': { + 'labelEn': 'PDU Field STRING', + 'type': 'PDU' + }, + 'fieldName': 'pdu_field_string', + 'flexFieldClassification': 'FLEX_FIELD_PDU', + 'roundNumber': 1 + } + ] + } + ] + } + ] + }, + 'totalHouseholdsCount': 1, + 'totalIndividualsCount': 3 + } + } +} + +snapshots['TestTargetPopulationQuery::test_simple_target_query_pdu_1_without_permission 1'] = { + 'data': { + 'targetPopulation': None + }, + 'errors': [ + { + 'locations': [ + { + 'column': 11, + 'line': 3 + } + ], + 'message': 'Permission Denied', + 'path': [ + 'targetPopulation' + ] + } + ] +} diff --git a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_update_target_population_mutation.py b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_update_target_population_mutation.py index cf2f33fc0d..2ddd60517b 100644 --- a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_update_target_population_mutation.py +++ b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_update_target_population_mutation.py @@ -114,8 +114,8 @@ 'name': 'with_permission_draft updated', 'status': 'OPEN', 'targetingCriteria': { - "flagExcludeIfActiveAdjudicationTicket": False, - "flagExcludeIfOnSanctionList": True, + 'flagExcludeIfActiveAdjudicationTicket': False, + 'flagExcludeIfOnSanctionList': True, 'rules': [ { 'filters': [ @@ -125,7 +125,7 @@ ], 'comparisonMethod': 'EQUALS', 'fieldName': 'size', - 'isFlexField': False + 'flexFieldClassification': 'NOT_FLEX_FIELD' } ] } diff --git a/backend/hct_mis_api/apps/targeting/tests/test_copy_target_population_mutation.py b/backend/hct_mis_api/apps/targeting/tests/test_copy_target_population_mutation.py index d16d47a05a..438729171f 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_copy_target_population_mutation.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_copy_target_population_mutation.py @@ -33,7 +33,7 @@ class TestCopyTargetPopulationMutation(APITestCase): filters{ comparisonMethod fieldName - isFlexField + flexFieldClassification arguments } } diff --git a/backend/hct_mis_api/apps/targeting/tests/test_create_target_population_mutation.py b/backend/hct_mis_api/apps/targeting/tests/test_create_target_population_mutation.py index b96a85a566..d23ec90de4 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_create_target_population_mutation.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_create_target_population_mutation.py @@ -5,8 +5,16 @@ from hct_mis_api.apps.account.fixtures import UserFactory from hct_mis_api.apps.account.permissions import Permissions from hct_mis_api.apps.core.base_test_case import APITestCase -from hct_mis_api.apps.core.fixtures import create_afghanistan -from hct_mis_api.apps.core.models import BusinessArea +from hct_mis_api.apps.core.fixtures import ( + FlexibleAttributeForPDUFactory, + PeriodicFieldDataFactory, + create_afghanistan, +) +from hct_mis_api.apps.core.models import ( + BusinessArea, + FlexibleAttribute, + PeriodicFieldData, +) from hct_mis_api.apps.household.fixtures import create_household from hct_mis_api.apps.household.models import Household from hct_mis_api.apps.program.fixtures import ProgramFactory @@ -36,7 +44,16 @@ class TestCreateTargetPopulationMutation(APITestCase): comparisonMethod fieldName arguments - isFlexField + flexFieldClassification + } + individualsFiltersBlocks{ + individualBlockFilters{ + comparisonMethod + fieldName + arguments + flexFieldClassification + roundNumber + } } } } @@ -63,6 +80,21 @@ def setUpTestData(cls) -> None: create_household( {"size": 4, "residence_status": "HOST", "program": cls.program}, ) + FlexibleAttribute.objects.create( + name="flex_field_1", + type=FlexibleAttribute.STRING, + associated_with=FlexibleAttribute.ASSOCIATED_WITH_INDIVIDUAL, + ) + pdu_data = PeriodicFieldDataFactory( + subtype=PeriodicFieldData.DECIMAL, + number_of_rounds=1, + rounds_names=["Round 1"], + ) + FlexibleAttributeForPDUFactory( + program=cls.program, + label="PDU Field 1", + pdu_data=pdu_data, + ) cls.variables = { "createTargetPopulationInput": { "name": "Example name 5", @@ -78,7 +110,7 @@ def setUpTestData(cls) -> None: "comparisonMethod": "EQUALS", "fieldName": "size", "arguments": [3], - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", } ] } @@ -111,7 +143,7 @@ def test_create_mutation(self, _: Any, permissions: List[Permissions]) -> None: "comparisonMethod": "EQUALS", "fieldName": "size", "arguments": [3], - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", } ] } @@ -149,7 +181,7 @@ def test_create_mutation_with_comparison_method_contains(self, _: Any, permissio "comparisonMethod": "CONTAINS", "arguments": [], "fieldName": "registration_data_import", - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", } ], "individualsFiltersBlocks": [], @@ -265,6 +297,81 @@ def test_create_mutation_target_by_id(self) -> None: variables=variables, ) + def test_create_mutation_with_flex_field(self) -> None: + self.create_user_role_with_permissions(self.user, [Permissions.TARGETING_CREATE], self.program.business_area) + + variables = { + "createTargetPopulationInput": { + "name": "Example name 5 ", + "businessAreaSlug": "afghanistan", + "programId": self.id_to_base64(self.program.id, "ProgramNode"), + "excludedIds": "", + "programCycleId": self.id_to_base64(self.program_cycle.id, "ProgramCycleNode"), + "targetingCriteria": { + "rules": [ + { + "filters": [], + "individualsFiltersBlocks": [ + { + "individualBlockFilters": [ + { + "comparisonMethod": "CONTAINS", + "arguments": ["Average"], + "fieldName": "flex_field_1", + "flexFieldClassification": "FLEX_FIELD_BASIC", + } + ] + } + ], + } + ] + }, + } + } + self.snapshot_graphql_request( + request_string=TestCreateTargetPopulationMutation.MUTATION_QUERY, + context={"user": self.user}, + variables=variables, + ) + + def test_create_mutation_with_pdu_flex_field(self) -> None: + self.create_user_role_with_permissions(self.user, [Permissions.TARGETING_CREATE], self.program.business_area) + + variables = { + "createTargetPopulationInput": { + "name": "Example name 5 ", + "businessAreaSlug": "afghanistan", + "programId": self.id_to_base64(self.program.id, "ProgramNode"), + "excludedIds": "", + "programCycleId": self.id_to_base64(self.program_cycle.id, "ProgramCycleNode"), + "targetingCriteria": { + "rules": [ + { + "filters": [], + "individualsFiltersBlocks": [ + { + "individualBlockFilters": [ + { + "comparisonMethod": "RANGE", + "arguments": ["2", "3.5"], + "fieldName": "pdu_field_1", + "flexFieldClassification": "FLEX_FIELD_PDU", + "roundNumber": "1", + } + ] + } + ], + } + ] + }, + } + } + self.snapshot_graphql_request( + request_string=TestCreateTargetPopulationMutation.MUTATION_QUERY, + context={"user": self.user}, + variables=variables, + ) + def test_create_targeting_if_program_cycle_finished(self) -> None: self.create_user_role_with_permissions(self.user, [Permissions.TARGETING_CREATE], self.program.business_area) self.program_cycle.status = Program.FINISHED diff --git a/backend/hct_mis_api/apps/targeting/tests/test_individual_block_filters.py b/backend/hct_mis_api/apps/targeting/tests/test_individual_block_filters.py index 75fd0219b6..82ae1cbf4b 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_individual_block_filters.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_individual_block_filters.py @@ -1,10 +1,16 @@ from django.core.management import call_command from django.test import TestCase -from hct_mis_api.apps.core.fixtures import create_afghanistan -from hct_mis_api.apps.core.models import BusinessArea +from hct_mis_api.apps.core.fixtures import ( + FlexibleAttributeForPDUFactory, + PeriodicFieldDataFactory, + create_afghanistan, +) +from hct_mis_api.apps.core.models import FlexibleAttribute, PeriodicFieldData from hct_mis_api.apps.household.fixtures import create_household_and_individuals from hct_mis_api.apps.household.models import FEMALE, MALE, Household +from hct_mis_api.apps.program.fixtures import ProgramFactory +from hct_mis_api.apps.targeting.choices import FlexFieldClassification from hct_mis_api.apps.targeting.models import ( TargetingCriteria, TargetingCriteriaQueryingBase, @@ -21,22 +27,24 @@ class TestIndividualBlockFilter(TestCase): @classmethod def setUpTestData(cls) -> None: call_command("loadflexfieldsattributes") - create_afghanistan() - business_area = BusinessArea.objects.first() + cls.business_area = create_afghanistan() + cls.program = ProgramFactory(business_area=cls.business_area, name="Test Program") (household, individuals) = create_household_and_individuals( { - "business_area": business_area, + "business_area": cls.business_area, }, [{"sex": "MALE", "marital_status": "MARRIED"}], ) cls.household_1_indiv = household + cls.individual_1 = individuals[0] (household, individuals) = create_household_and_individuals( { - "business_area": business_area, + "business_area": cls.business_area, }, [{"sex": "MALE", "marital_status": "SINGLE"}, {"sex": "FEMALE", "marital_status": "MARRIED"}], ) cls.household_2_indiv = household + cls.individual_2 = individuals[0] def test_all_individuals_are_female(self) -> None: queryset = Household.objects.all() @@ -125,3 +133,228 @@ def test_two_separate_blocks_on_mixins(self) -> None: query = query.filter(tc.get_query()) self.assertEqual(query.count(), 1) self.assertEqual(query.first().id, self.household_2_indiv.id) + + def test_filter_on_flex_field_not_exist(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + query = Household.objects.all() + flex_field_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="CONTAINS", + field_name="flex_field_2", + arguments=["Average"], + flex_field_classification=FlexFieldClassification.FLEX_FIELD_BASIC, + ) + flex_field_filter.save() + + with self.assertRaises(Exception) as e: + query.filter(tc.get_query()) + self.assertIn( + "There is no Flex Field Attributes associated with this fieldName flex_field_2", + str(e.exception), + ) + + def test_filter_on_flex_field(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + FlexibleAttribute.objects.create( + name="flex_field_1", + type=FlexibleAttribute.STRING, + associated_with=FlexibleAttribute.ASSOCIATED_WITH_INDIVIDUAL, + ) + query = Household.objects.all() + flex_field_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="CONTAINS", + field_name="flex_field_1", + arguments=["Average"], + flex_field_classification=FlexFieldClassification.FLEX_FIELD_BASIC, + ) + flex_field_filter.save() + query = query.filter(tc.get_query()) + self.assertEqual(query.count(), 0) + + self.individual_1.flex_fields["flex_field_1"] = "Average value" + self.individual_1.save() + + query = query.filter(tc.get_query()) + + self.assertEqual(query.count(), 1) + self.assertEqual(query.first().id, self.household_1_indiv.id) + + def test_filter_on_pdu_flex_field_not_exist(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + query = Household.objects.all() + pdu_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="RANGE", + field_name="pdu_field_1", + arguments=["2", "3"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + pdu_filter.save() + + with self.assertRaises(Exception) as e: + query.filter(tc.get_query()) + self.assertIn( + "There is no PDU Flex Field Attribute associated with this fieldName pdu_field_1 in program Test Program", + str(e.exception), + ) + + def test_filter_on_pdu_flex_field_no_round_number(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + pdu_data = PeriodicFieldDataFactory( + subtype=PeriodicFieldData.DECIMAL, + number_of_rounds=2, + rounds_names=["Round 1", "Round 2"], + ) + FlexibleAttributeForPDUFactory( + program=self.program, + label="PDU Field 1", + pdu_data=pdu_data, + ) + query = Household.objects.all() + pdu_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="RANGE", + field_name="pdu_field_1", + arguments=["2", "3"], + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + pdu_filter.save() + + with self.assertRaises(Exception) as e: + query.filter(tc.get_query()) + self.assertIn( + "Round number is missing for PDU Flex Field Attribute pdu_field_1", + str(e.exception), + ) + + def test_filter_on_pdu_flex_field_incorrect_round_number(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + pdu_data = PeriodicFieldDataFactory( + subtype=PeriodicFieldData.DECIMAL, + number_of_rounds=2, + rounds_names=["Round 1", "Round 2"], + ) + FlexibleAttributeForPDUFactory( + program=self.program, + label="PDU Field 1", + pdu_data=pdu_data, + ) + query = Household.objects.all() + pdu_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="RANGE", + field_name="pdu_field_1", + arguments=["2", "3"], + round_number=3, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + pdu_filter.save() + + with self.assertRaises(Exception) as e: + query.filter(tc.get_query()) + self.assertIn( + "Round number 3 is greater than the number of rounds for PDU Flex Field Attribute pdu_field_1", + str(e.exception), + ) + + def test_filter_on_pdu_flex_field(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + pdu_data = PeriodicFieldDataFactory( + subtype=PeriodicFieldData.DECIMAL, + number_of_rounds=2, + rounds_names=["Round 1", "Round 2"], + ) + FlexibleAttributeForPDUFactory( + program=self.program, + label="PDU Field 1", + pdu_data=pdu_data, + ) + query = Household.objects.all() + pdu_filter = TargetingIndividualBlockRuleFilter.objects.create( + individuals_filters_block=individuals_filters_block, + comparison_method="RANGE", + field_name="pdu_field_1", + arguments=["2", "3"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + pdu_filter.save() + + self.individual_1.flex_fields = {"pdu_field_1": {"1": {"value": None}, "2": {"value": None}}} + self.individual_1.save() + self.individual_2.flex_fields = { + "pdu_field_1": {"1": {"value": 1, "collection_date": "2021-01-01"}, "2": {"value": None}} + } + self.individual_2.save() + + query = query.filter(tc.get_query()) + self.assertEqual(query.count(), 0) + + self.individual_1.flex_fields["pdu_field_1"]["1"] = {"value": 2.5, "collection_date": "2021-01-01"} + self.individual_1.save() + + query = query.filter(tc.get_query()) + self.assertEqual(query.count(), 1) + self.assertEqual(query.first().id, self.household_1_indiv.id) diff --git a/backend/hct_mis_api/apps/targeting/tests/test_target_query.py b/backend/hct_mis_api/apps/targeting/tests/test_target_query.py index 4baea64e6e..baf2870bb7 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_target_query.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_target_query.py @@ -5,15 +5,24 @@ from hct_mis_api.apps.account.fixtures import PartnerFactory, UserFactory from hct_mis_api.apps.account.permissions import Permissions from hct_mis_api.apps.core.base_test_case import APITestCase -from hct_mis_api.apps.core.fixtures import create_afghanistan -from hct_mis_api.apps.core.models import BusinessArea +from hct_mis_api.apps.core.fixtures import ( + FlexibleAttributeForPDUFactory, + PeriodicFieldDataFactory, + create_afghanistan, +) +from hct_mis_api.apps.core.models import BusinessArea, PeriodicFieldData from hct_mis_api.apps.household.fixtures import create_household +from hct_mis_api.apps.household.models import DISABLED +from hct_mis_api.apps.periodic_data_update.utils import populate_pdu_with_null_values from hct_mis_api.apps.program.fixtures import ProgramCycleFactory, ProgramFactory from hct_mis_api.apps.program.models import Program +from hct_mis_api.apps.targeting.choices import FlexFieldClassification from hct_mis_api.apps.targeting.models import ( TargetingCriteria, TargetingCriteriaRule, TargetingCriteriaRuleFilter, + TargetingIndividualBlockRuleFilter, + TargetingIndividualRuleFilterBlock, TargetPopulation, ) from hct_mis_api.apps.targeting.services.targeting_stats_refresher import full_rebuild @@ -68,13 +77,27 @@ class TestTargetPopulationQuery(APITestCase): filters{ comparisonMethod fieldName - isFlexField + flexFieldClassification arguments fieldAttribute{ labelEn type } } + individualsFiltersBlocks{ + individualBlockFilters{ + comparisonMethod + fieldName + arguments + flexFieldClassification + roundNumber + fieldAttribute + { + labelEn + type + } + } + } } } } @@ -106,6 +129,7 @@ def setUpTestData(cls) -> None: cls.user = UserFactory(partner=cls.partner, first_name="Test", last_name="User") user_first = UserFactory(partner=cls.partner, first_name="First", last_name="User") user_second = UserFactory(partner=cls.partner, first_name="Second", last_name="User") + user_third = UserFactory(partner=cls.partner, first_name="Third", last_name="User") targeting_criteria = cls.get_targeting_criteria_for_rule( {"field_name": "size", "arguments": [2], "comparison_method": "EQUALS"} ) @@ -149,6 +173,86 @@ def setUpTestData(cls) -> None: cls.target_population_size_1_approved = full_rebuild(cls.target_population_size_1_approved) cls.target_population_size_1_approved.save() + pdu_data_string = PeriodicFieldDataFactory( + subtype=PeriodicFieldData.STRING, + number_of_rounds=2, + rounds_names=["Round 1", "Round 2"], + ) + cls.pdu_field_string = FlexibleAttributeForPDUFactory( + program=cls.program, + label="PDU Field STRING", + pdu_data=pdu_data_string, + ) + (household, individuals) = create_household( + {"size": 3, "residence_status": "HOST", "business_area": cls.business_area, "program": cls.program}, + ) + individual_with_pdu_value = individuals[0] + populate_pdu_with_null_values(cls.program, individual_with_pdu_value.flex_fields) + individual_with_pdu_value.flex_fields[cls.pdu_field_string.name]["1"]["value"] = "some" + individual_with_pdu_value.save() + targeting_criteria = TargetingCriteria() + targeting_criteria.save() + rule = TargetingCriteriaRule(targeting_criteria=targeting_criteria) + rule.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=rule, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="EQUALS", + field_name=cls.pdu_field_string.name, + arguments=["some"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + rule_filter.save() + cls.target_population_with_pdu_filter = TargetPopulation( + name="target_population_with_pdu_filter", + created_by=user_third, + targeting_criteria=targeting_criteria, + status=TargetPopulation.STATUS_LOCKED, + business_area=cls.business_area, + program=cls.program, + ) + cls.target_population_with_pdu_filter.save() + cls.target_population_with_pdu_filter = full_rebuild(cls.target_population_with_pdu_filter) + cls.target_population_with_pdu_filter.save() + + (household, individuals) = create_household( + {"size": 3, "residence_status": "HOST", "business_area": cls.business_area, "program": cls.program}, + ) + individual = individuals[0] + individual.disability = DISABLED + individual.save() + targeting_criteria = TargetingCriteria() + targeting_criteria.save() + rule = TargetingCriteriaRule(targeting_criteria=targeting_criteria) + rule.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=rule, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="EQUALS", + field_name="disability", + arguments=["disabled"], + flex_field_classification=FlexFieldClassification.NOT_FLEX_FIELD, + ) + rule_filter.save() + cls.target_population_with_individual_filter = TargetPopulation( + name="target_population_with_individual_filter", + created_by=user_third, + targeting_criteria=targeting_criteria, + status=TargetPopulation.STATUS_LOCKED, + business_area=cls.business_area, + program=cls.program, + ) + cls.target_population_with_individual_filter.save() + cls.target_population_with_individual_filter = full_rebuild(cls.target_population_with_individual_filter) + cls.target_population_with_individual_filter.save() + @staticmethod def get_targeting_criteria_for_rule(rule_filter: Dict) -> TargetingCriteria: targeting_criteria = TargetingCriteria() @@ -245,6 +349,68 @@ def test_simple_target_query_next(self, _: Any, permissions: List[Permissions]) }, ) + @parameterized.expand( + [ + ( + "with_permission", + [Permissions.TARGETING_VIEW_DETAILS], + ), + ( + "without_permission", + [], + ), + ] + ) + def test_simple_target_query_pdu(self, _: Any, permissions: List[Permissions]) -> None: + self.create_user_role_with_permissions(self.user, permissions, self.business_area, self.program) + self.snapshot_graphql_request( + request_string=TestTargetPopulationQuery.TARGET_POPULATION_QUERY, + context={ + "user": self.user, + "headers": { + "Business-Area": self.business_area.slug, + "Program": self.id_to_base64(self.program.id, "ProgramNode"), + }, + }, + variables={ + "id": self.id_to_base64( + self.target_population_with_pdu_filter.id, + "TargetPopulationNode", + ) + }, + ) + + @parameterized.expand( + [ + ( + "with_permission", + [Permissions.TARGETING_VIEW_DETAILS], + ), + ( + "without_permission", + [], + ), + ] + ) + def test_simple_target_query_individual_filter(self, _: Any, permissions: List[Permissions]) -> None: + self.create_user_role_with_permissions(self.user, permissions, self.business_area, self.program) + self.snapshot_graphql_request( + request_string=TestTargetPopulationQuery.TARGET_POPULATION_QUERY, + context={ + "user": self.user, + "headers": { + "Business-Area": self.business_area.slug, + "Program": self.id_to_base64(self.program.id, "ProgramNode"), + }, + }, + variables={ + "id": self.id_to_base64( + self.target_population_with_individual_filter.id, + "TargetPopulationNode", + ) + }, + ) + def test_all_targets_query_filter_by_cycle(self) -> None: self.create_user_role_with_permissions( self.user, [Permissions.TARGETING_VIEW_LIST], self.business_area, self.program diff --git a/backend/hct_mis_api/apps/targeting/tests/test_targeting_criteria.py b/backend/hct_mis_api/apps/targeting/tests/test_targeting_criteria.py index 4c25ce86da..f262f2fbbd 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_targeting_criteria.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_targeting_criteria.py @@ -71,7 +71,7 @@ def test_size(self) -> None: "comparison_method": "EQUALS", "arguments": [2], "field_name": "size", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", } ).get_query() ) @@ -88,7 +88,7 @@ def test_residence_status(self) -> None: "comparison_method": "EQUALS", "arguments": ["REFUGEE"], "field_name": "residence_status", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", } ).get_query() ) @@ -105,7 +105,7 @@ def test_flex_field_variables(self) -> None: "comparison_method": "EQUALS", "arguments": ["0"], "field_name": "unaccompanied_child_h_f", - "is_flex_field": True, + "flex_field_classification": "FLEX_FIELD_BASIC", } ).get_query() ) @@ -122,7 +122,7 @@ def test_select_many_variables(self) -> None: "comparison_method": "CONTAINS", "arguments": ["other_public", "pharmacy", "other_private"], "field_name": "treatment_facility_h_f", - "is_flex_field": True, + "flex_field_classification": "FLEX_FIELD_BASIC", } ).get_query() ) @@ -214,13 +214,13 @@ def test_marital_status(self) -> None: "comparison_method": "EQUALS", "arguments": ["MARRIED"], "field_name": "marital_status", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", }, { "comparison_method": "EQUALS", "arguments": ["MALE"], "field_name": "sex", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", }, ] ).get_query() @@ -239,7 +239,7 @@ def test_observed_disability(self) -> None: "comparison_method": "CONTAINS", "arguments": ["COMMUNICATING", "HEARING", "MEMORY", "SEEING", "WALKING", "SELF_CARE"], "field_name": "observed_disability", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", }, ] ).get_query() @@ -258,7 +258,7 @@ def test_ranges(self) -> None: "comparison_method": "RANGE", "arguments": [20, 25], "field_name": "age", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", }, ] ).get_query() @@ -276,7 +276,7 @@ def test_ranges(self) -> None: "comparison_method": "RANGE", "arguments": [22, 26], "field_name": "age", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", }, ] ).get_query() @@ -294,7 +294,7 @@ def test_ranges(self) -> None: "comparison_method": "LESS_THAN", "arguments": [20], "field_name": "age", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", }, ] ).get_query() @@ -312,7 +312,7 @@ def test_ranges(self) -> None: "comparison_method": "LESS_THAN", "arguments": [24], "field_name": "age", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", }, ] ).get_query() @@ -330,7 +330,7 @@ def test_ranges(self) -> None: "comparison_method": "GREATER_THAN", "arguments": [20], "field_name": "age", - "is_flex_field": False, + "flex_field_classification": "NOT_FLEX_FIELD", }, ] ).get_query() diff --git a/backend/hct_mis_api/apps/targeting/tests/test_targeting_criteria_rule_filter.py b/backend/hct_mis_api/apps/targeting/tests/test_targeting_criteria_rule_filter.py index d7b6aa1ff6..c457a025a6 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_targeting_criteria_rule_filter.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_targeting_criteria_rule_filter.py @@ -9,16 +9,26 @@ from freezegun import freeze_time from pytz import utc -from hct_mis_api.apps.core.fixtures import create_afghanistan -from hct_mis_api.apps.core.models import BusinessArea +from hct_mis_api.apps.core.fixtures import ( + FlexibleAttributeForPDUFactory, + PeriodicFieldDataFactory, + create_afghanistan, +) +from hct_mis_api.apps.core.models import PeriodicFieldData from hct_mis_api.apps.household.fixtures import ( create_household, create_household_and_individuals, ) from hct_mis_api.apps.household.models import Household, Individual +from hct_mis_api.apps.program.fixtures import ProgramFactory +from hct_mis_api.apps.targeting.choices import FlexFieldClassification from hct_mis_api.apps.targeting.models import ( + TargetingCriteria, + TargetingCriteriaRule, TargetingCriteriaRuleFilter, TargetingIndividualBlockRuleFilter, + TargetingIndividualRuleFilterBlock, + TargetPopulation, ) @@ -26,8 +36,7 @@ class TargetingCriteriaRuleFilterTestCase(TestCase): @classmethod def setUpTestData(cls) -> None: households = [] - create_afghanistan() - business_area = BusinessArea.objects.first() + business_area = create_afghanistan() (household, individuals) = create_household_and_individuals( { "size": 1, @@ -296,8 +305,7 @@ class TargetingCriteriaFlexRuleFilterTestCase(TestCase): @classmethod def setUpTestData(cls) -> None: call_command("loadflexfieldsattributes") - create_afghanistan() - business_area = BusinessArea.objects.first() + business_area = create_afghanistan() (household, individuals) = create_household( { "size": 1, @@ -331,7 +339,7 @@ def test_rule_filter_household_total_households_4(self) -> None: comparison_method="EQUALS", field_name="total_households_h_f", arguments=[4], - is_flex_field=True, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_BASIC, ) query = rule_filter.get_query() queryset = Household.objects.filter(query) @@ -343,7 +351,7 @@ def test_rule_filter_select_multiple_treatment_facility(self) -> None: comparison_method="CONTAINS", field_name="treatment_facility_h_f", arguments=["other_public", "private_doctor"], - is_flex_field=True, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_BASIC, ) query = rule_filter.get_query() queryset = Household.objects.filter(query) @@ -354,7 +362,7 @@ def test_rule_filter_select_multiple_treatment_facility_2(self) -> None: comparison_method="CONTAINS", field_name="treatment_facility_h_f", arguments=["other_public", "government_health_center"], - is_flex_field=True, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_BASIC, ) query = rule_filter.get_query() queryset = Household.objects.filter(query) @@ -365,7 +373,7 @@ def test_rule_filter_select_multiple_treatment_facility_not_contains(self) -> No comparison_method="NOT_CONTAINS", field_name="treatment_facility_h_f", arguments=["other_public", "government_health_center"], - is_flex_field=True, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_BASIC, ) query = rule_filter.get_query() queryset = Household.objects.filter(query) @@ -376,8 +384,487 @@ def test_rule_filter_string_contains(self) -> None: comparison_method="CONTAINS", field_name="other_treatment_facility_h_f", arguments=["other"], - is_flex_field=True, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_BASIC, ) query = rule_filter.get_query() queryset = Household.objects.filter(query) self.assertEqual(queryset.count(), 1) + + +class TargetingCriteriaPDUFlexRuleFilterTestCase(TestCase): + @classmethod + def setUpTestData(cls) -> None: + call_command("loadflexfieldsattributes") + business_area = create_afghanistan() + cls.program = ProgramFactory(name="Test Program for PDU Flex Rule Filter", business_area=business_area) + + pdu_data_string = PeriodicFieldDataFactory( + subtype=PeriodicFieldData.STRING, + number_of_rounds=2, + rounds_names=["Round 1", "Round 2"], + ) + cls.pdu_field_string = FlexibleAttributeForPDUFactory( + program=cls.program, + label="PDU Field STRING", + pdu_data=pdu_data_string, + ) + + pdu_data_decimal = PeriodicFieldDataFactory( + subtype=PeriodicFieldData.DECIMAL, + number_of_rounds=1, + rounds_names=["Round 1"], + ) + cls.pdu_field_decimal = FlexibleAttributeForPDUFactory( + program=cls.program, + label="PDU Field DECIMAL", + pdu_data=pdu_data_decimal, + ) + + pdu_data_date = PeriodicFieldDataFactory( + subtype=PeriodicFieldData.DATE, + number_of_rounds=1, + rounds_names=["Round 1"], + ) + cls.pdu_field_date = FlexibleAttributeForPDUFactory( + program=cls.program, + label="PDU Field DATE", + pdu_data=pdu_data_date, + ) + + pdu_data_boolean = PeriodicFieldDataFactory( + subtype=PeriodicFieldData.BOOL, + number_of_rounds=1, + rounds_names=["Round 1"], + ) + cls.pdu_field_boolean = FlexibleAttributeForPDUFactory( + program=cls.program, + label="PDU Field BOOLEAN", + pdu_data=pdu_data_boolean, + ) + + (household, individuals) = create_household( + { + "size": 1, + "business_area": business_area, + "program": cls.program, + }, + { + "flex_fields": { + cls.pdu_field_string.name: {"1": {"value": None}, "2": {"value": None}}, + cls.pdu_field_decimal.name: {"1": {"value": 2.5}}, + cls.pdu_field_date.name: {"1": {"value": "2020-10-10"}}, + cls.pdu_field_boolean.name: {"1": {"value": True}}, + }, + "business_area": business_area, + }, + ) + cls.individual1 = individuals[0] + cls.other_treatment_facility = household + (household, individuals) = create_household( + { + "size": 1, + "business_area": business_area, + "program": cls.program, + }, + { + "flex_fields": { + cls.pdu_field_string.name: { + "1": {"value": "some value", "collection_date": "2020-10-10"}, + "2": {"value": None}, + }, + cls.pdu_field_decimal.name: {"1": {"value": 3}}, + cls.pdu_field_date.name: {"1": {"value": None}}, + cls.pdu_field_boolean.name: {"1": {"value": True}}, + }, + "business_area": business_area, + }, + ) + cls.individual2 = individuals[0] + (household, individuals) = create_household( + { + "size": 1, + "business_area": business_area, + "program": cls.program, + }, + { + "flex_fields": { + cls.pdu_field_string.name: { + "1": {"value": "different value", "collection_date": "2020-10-10"}, + "2": {"value": None}, + }, + cls.pdu_field_decimal.name: {"1": {"value": 4}}, + cls.pdu_field_date.name: {"1": {"value": "2020-02-10"}}, + cls.pdu_field_boolean.name: {"1": {"value": None}}, + }, + "business_area": business_area, + }, + ) + cls.individual3 = individuals[0] + (household, individuals) = create_household( + { + "size": 1, + "business_area": business_area, + "program": cls.program, + }, + { + "flex_fields": { + cls.pdu_field_string.name: { + "1": {"value": "other value", "collection_date": "2020-10-10"}, + "2": {"value": None}, + }, + cls.pdu_field_decimal.name: {"1": {"value": None}}, + cls.pdu_field_date.name: {"1": {"value": "2022-10-10"}}, + cls.pdu_field_boolean.name: {"1": {"value": False}}, + }, + "business_area": business_area, + }, + ) + cls.individual4 = individuals[0] + cls.individuals = [cls.individual1, cls.individual2, cls.individual3, cls.individual4] + + def get_individuals_queryset(self) -> QuerySet[Household]: + return Individual.objects.filter(pk__in=[ind.pk for ind in self.individuals]) + + def test_rule_filter_pdu_string_contains(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="CONTAINS", + field_name=self.pdu_field_string.name, + arguments=["some"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 1) + self.assertIn(self.individual2, queryset) + + def test_rule_filter_pdu_string_is_null(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="IS_NULL", + field_name=self.pdu_field_string.name, + arguments=[None], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 1) + self.assertIn(self.individual1, queryset) + + def test_rule_filter_pdu_decimal_range(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="RANGE", + field_name=self.pdu_field_decimal.name, + arguments=["2", "3"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 2) + self.assertIn(self.individual1, queryset) + self.assertIn(self.individual2, queryset) + + def test_rule_filter_pdu_decimal_greater_than(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="GREATER_THAN", + field_name=self.pdu_field_decimal.name, + arguments=["2.5"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 3) + self.assertIn(self.individual1, queryset) + self.assertIn(self.individual2, queryset) + self.assertIn(self.individual3, queryset) + + def test_rule_filter_pdu_decimal_less_than(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="LESS_THAN", + field_name=self.pdu_field_decimal.name, + arguments=["2.5"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + queryset = self.get_individuals_queryset().filter(query).distinct() + + self.assertEqual(queryset.count(), 1) + self.assertIn(self.individual1, queryset) + + def test_rule_filter_pdu_decimal_is_null(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="IS_NULL", + field_name=self.pdu_field_decimal.name, + arguments=[None], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 1) + self.assertIn(self.individual4, queryset) + + def test_rule_filter_pdu_date_range(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="RANGE", + field_name=self.pdu_field_date.name, + arguments=["2020-02-10", "2020-10-10"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 2) + self.assertIn(self.individual1, queryset) + self.assertIn(self.individual3, queryset) + + def test_rule_filter_pdu_date_greater_than(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="GREATER_THAN", + field_name=self.pdu_field_date.name, + arguments=["2020-10-11"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 1) + self.assertIn(self.individual4, queryset) + + def test_rule_filter_pdu_date_less_than(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="LESS_THAN", + field_name=self.pdu_field_date.name, + arguments=["2020-10-11"], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + queryset = self.get_individuals_queryset().filter(query).distinct() + + self.assertEqual(queryset.count(), 2) + self.assertIn(self.individual1, queryset) + self.assertIn(self.individual3, queryset) + + def test_rule_filter_pdu_date_is_null(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="IS_NULL", + field_name=self.pdu_field_date.name, + arguments=[None], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 1) + self.assertIn(self.individual2, queryset) + + def test_rule_filter_pdu_boolean_true(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="EQUALS", + field_name=self.pdu_field_boolean.name, + arguments=[True], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 2) + self.assertIn(self.individual1, queryset) + self.assertIn(self.individual2, queryset) + + def test_rule_filter_pdu_boolean_false(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="EQUALS", + field_name=self.pdu_field_boolean.name, + arguments=[False], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 1) + self.assertIn(self.individual4, queryset) + + def test_rule_filter_pdu_boolean_is_null(self) -> None: + tp = TargetPopulation(program=self.program) + tc = TargetingCriteria() + tc.target_population = tp + tc.save() + tcr = TargetingCriteriaRule() + tcr.targeting_criteria = tc + tcr.save() + individuals_filters_block = TargetingIndividualRuleFilterBlock( + targeting_criteria_rule=tcr, target_only_hoh=False + ) + individuals_filters_block.save() + rule_filter = TargetingIndividualBlockRuleFilter( + individuals_filters_block=individuals_filters_block, + comparison_method="IS_NULL", + field_name=self.pdu_field_boolean.name, + arguments=[None], + round_number=1, + flex_field_classification=FlexFieldClassification.FLEX_FIELD_PDU, + ) + query = rule_filter.get_query() + + queryset = self.get_individuals_queryset().filter(query).distinct() + self.assertEqual(queryset.count(), 1) + self.assertIn(self.individual3, queryset) diff --git a/backend/hct_mis_api/apps/targeting/tests/test_targeting_validators.py b/backend/hct_mis_api/apps/targeting/tests/test_targeting_validators.py index d8be7b3838..6438638ec5 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_targeting_validators.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_targeting_validators.py @@ -47,14 +47,14 @@ def test_TargetingCriteriaInputValidator(self) -> None: validator = TargetingCriteriaInputValidator create_household({"unicef_id": "HH-1", "size": 1}, {"unicef_id": "IND-1"}) with self.assertRaisesMessage( - ValidationError, "Target criteria can has only filters or ids, not possible to has both" + ValidationError, "Target criteria can only have filters or ids, not possible to have both" ): self._update_program(self.program_standard) validator.validate( {"rules": ["Rule1"], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.program_standard ) - with self.assertRaisesMessage(ValidationError, "Target criteria can has only individual ids"): + with self.assertRaisesMessage(ValidationError, "Target criteria can only have individual ids"): self._update_program(self.program_social) validator.validate({"rules": [], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.program_social) @@ -63,7 +63,7 @@ def test_TargetingCriteriaInputValidator(self) -> None: {"rules": [], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.program_standard_ind_only ) - with self.assertRaisesMessage(ValidationError, "Target criteria can has only household ids"): + with self.assertRaisesMessage(ValidationError, "Target criteria can only have household ids"): self._update_program(self.program_standard_hh_only) validator.validate( {"rules": [], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.program_standard_hh_only diff --git a/backend/hct_mis_api/apps/targeting/tests/test_update_target_population_mutation.py b/backend/hct_mis_api/apps/targeting/tests/test_update_target_population_mutation.py index 2a21b412d3..4587db8f07 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_update_target_population_mutation.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_update_target_population_mutation.py @@ -35,7 +35,7 @@ comparisonMethod fieldName arguments - isFlexField + flexFieldClassification } } } @@ -55,7 +55,7 @@ "comparisonMethod": "EQUALS", "fieldName": "size", "arguments": [3], - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", } ] } @@ -74,7 +74,7 @@ "comparisonMethod": "EQUALS", "fieldName": "size", "arguments": [3, 3], - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", } ] } @@ -92,7 +92,7 @@ "comparisonMethod": "CONTAINS", "fieldName": "size", "arguments": [3], - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", } ] } @@ -110,7 +110,7 @@ "comparisonMethod": "BLABLA", "fieldName": "size", "arguments": [3], - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", } ] } @@ -128,7 +128,7 @@ "comparisonMethod": "EQUALS", "fieldName": "foo_bar", "arguments": [3], - "isFlexField": True, + "flexFieldClassification": "FLEX_FIELD_BASIC", } ] } @@ -146,7 +146,7 @@ "comparisonMethod": "EQUALS", "fieldName": "foo_bar", "arguments": [3], - "isFlexField": False, + "flexFieldClassification": "NOT_FLEX_FIELD", } ] } diff --git a/backend/hct_mis_api/apps/targeting/validators.py b/backend/hct_mis_api/apps/targeting/validators.py index c1f56eda09..5aebe5cf8a 100644 --- a/backend/hct_mis_api/apps/targeting/validators.py +++ b/backend/hct_mis_api/apps/targeting/validators.py @@ -10,6 +10,7 @@ from hct_mis_api.apps.core.validators import BaseValidator from hct_mis_api.apps.household.models import Household, Individual from hct_mis_api.apps.program.models import Program +from hct_mis_api.apps.targeting.choices import FlexFieldClassification from hct_mis_api.apps.targeting.models import ( TargetingCriteriaRuleFilter, TargetPopulation, @@ -74,9 +75,9 @@ def validate(target_population: TargetPopulation) -> None: class TargetingCriteriaRuleFilterInputValidator: @staticmethod - def validate(rule_filter: Any) -> None: - is_flex_field = rule_filter.is_flex_field - if not is_flex_field: + def validate(rule_filter: Any, program: Program) -> None: + flex_field_classification = rule_filter.flex_field_classification + if flex_field_classification == FlexFieldClassification.NOT_FLEX_FIELD: attributes = FieldFactory.from_scope(Scope.TARGETING).to_dict_by("name") attribute = attributes.get(rule_filter.field_name) if attribute is None: @@ -84,9 +85,9 @@ def validate(rule_filter: Any) -> None: raise ValidationError( f"Can't find any core field attribute associated with {rule_filter.field_name} field name" ) - else: + elif flex_field_classification == FlexFieldClassification.FLEX_FIELD_BASIC: try: - attribute = FlexibleAttribute.objects.get(name=rule_filter.field_name) + attribute = FlexibleAttribute.objects.get(name=rule_filter.field_name, program=None) except FlexibleAttribute.DoesNotExist: logger.exception( f"Can't find any flex field attribute associated with {rule_filter.field_name} field name", @@ -94,6 +95,16 @@ def validate(rule_filter: Any) -> None: raise ValidationError( f"Can't find any flex field attribute associated with {rule_filter.field_name} field name" ) + else: + try: + attribute = FlexibleAttribute.objects.get(name=rule_filter.field_name, program=program) + except FlexibleAttribute.DoesNotExist: # pragma: no cover + logger.exception( + f"Can't find PDU flex field attribute associated with {rule_filter.field_name} field name in program {program.name}", + ) + raise ValidationError( + f"Can't find PDU flex field attribute associated with {rule_filter.field_name} field name in program {program.name}", + ) comparison_attribute = TargetingCriteriaRuleFilter.COMPARISON_ATTRIBUTES.get(rule_filter.comparison_method) if comparison_attribute is None: logger.error(f"Unknown comparison method - {rule_filter.comparison_method}") @@ -109,7 +120,10 @@ def validate(rule_filter: Any) -> None: f"Comparison method '{rule_filter.comparison_method}' " f"expected {args_count} arguments, {given_args_count} given" ) - if get_attr_value("type", attribute) not in comparison_attribute.get("supported_types"): + type = get_attr_value("type", attribute, None) + if type == FlexibleAttribute.PDU: + type = attribute.pdu_data.subtype + if type not in comparison_attribute.get("supported_types"): raise ValidationError( f"{rule_filter.field_name} is '{get_attr_value('type', attribute)}' type filter " f"and does not accept '{rule_filter.comparison_method}' comparison method" @@ -118,10 +132,10 @@ def validate(rule_filter: Any) -> None: class TargetingCriteriaRuleInputValidator: @staticmethod - def validate(rule: "Rule") -> None: + def validate(rule: "Rule", program: "Program") -> None: total_len = 0 filters = rule.get("filters") - individuals_filters_blocks = rule.get("individuals_filters_blocks") + individuals_filters_blocks = rule.get("individuals_filters_blocks", []) if filters is not None: total_len += len(filters) if individuals_filters_blocks is not None: @@ -131,7 +145,11 @@ def validate(rule: "Rule") -> None: logger.error("There should be at least 1 filter or block in rules") raise ValidationError("There should be at least 1 filter or block in rules") for rule_filter in filters: - TargetingCriteriaRuleFilterInputValidator.validate(rule_filter) + TargetingCriteriaRuleFilterInputValidator.validate(rule_filter=rule_filter, program=program) + for individuals_filters_block in individuals_filters_blocks: + individual_block_filters = individuals_filters_block.get("individual_block_filters", []) + for individual_block_filter in individual_block_filters: + TargetingCriteriaRuleFilterInputValidator.validate(rule_filter=individual_block_filter, program=program) class TargetingCriteriaInputValidator: @@ -142,17 +160,17 @@ def validate(targeting_criteria: Dict, program: Program) -> None: household_ids = targeting_criteria.get("household_ids") individual_ids = targeting_criteria.get("individual_ids") if rules and (household_ids or individual_ids): - logger.error("Target criteria can has only filters or ids, not possible to has both") - raise ValidationError("Target criteria can has only filters or ids, not possible to has both") + logger.error("Target criteria can only have filters or ids, not possible to have both") + raise ValidationError("Target criteria can only have filters or ids, not possible to have both") if household_ids and not ( program_dct.household_filters_available or program_dct.type == DataCollectingType.Type.SOCIAL ): - logger.error("Target criteria can has only individual ids") - raise ValidationError("Target criteria can has only individual ids") + logger.error("Target criteria can only have individual ids") + raise ValidationError("Target criteria can only have individual ids") if individual_ids and not program_dct.individual_filters_available: - logger.error("Target criteria can has only household ids") - raise ValidationError("Target criteria can has only household ids") + logger.error("Target criteria can only have household ids") + raise ValidationError("Target criteria can only have household ids") if household_ids: ids_list = household_ids.split(",") @@ -176,4 +194,4 @@ def validate(targeting_criteria: Dict, program: Program) -> None: if not household_ids and not individual_ids: for rule in rules: - TargetingCriteriaRuleInputValidator.validate(rule) + TargetingCriteriaRuleInputValidator.validate(rule=rule, program=program) diff --git a/backend/selenium_tests/page_object/targeting/targeting_create.py b/backend/selenium_tests/page_object/targeting/targeting_create.py index 2db43d8676..d870218b44 100644 --- a/backend/selenium_tests/page_object/targeting/targeting_create.py +++ b/backend/selenium_tests/page_object/targeting/targeting_create.py @@ -52,6 +52,35 @@ class TargetingCreate(BaseComponents): autocompleteTargetCriteriaValues = 'div[data-cy="autocomplete-target-criteria-values"]' selectMany = 'div[data-cy="select-many"]' buttonEdit = 'button[data-cy="button-edit"]' + datePickerFilter = 'div[data-cy="date-picker-filter"]' + boolField = 'div[data-cy="bool-field"]' + textField = 'div[data-cy="string-textfield"]' + selectIndividualsFiltersBlocksRoundNumber = ( + 'div[data-cy="select-individualsFiltersBlocks[{}].individualBlockFilters[{}].roundNumber"]' + ) + selectRoundOption = 'li[data-cy="select-option-{}"]' + selectIndividualsFiltersBlocksIsNull = ( + 'span[data-cy="input-individualsFiltersBlocks[{}].individualBlockFilters[{}].isNull"]' + ) + inputIndividualsFiltersBlocksValueFrom = ( + 'input[data-cy="input-individualsFiltersBlocks[{}].individualBlockFilters[{}].value.from"]' + ) + inputIndividualsFiltersBlocksValueTo = ( + 'input[data-cy="input-individualsFiltersBlocks[{}].individualBlockFilters[{}].value.to"]' + ) + inputDateIndividualsFiltersBlocksValueFrom = ( + 'input[data-cy="date-input-individualsFiltersBlocks[{}].individualBlockFilters[{}].value.from"]' + ) + inputDateIndividualsFiltersBlocksValueTo = ( + 'input[data-cy="date-input-individualsFiltersBlocks[{}].individualBlockFilters[{}].value.to"]' + ) + inputIndividualsFiltersBlocksValue = ( + 'input[data-cy="input-individualsFiltersBlocks[{}].individualBlockFilters[{}].value"]' + ) + selectIndividualsFiltersBlocksValue = ( + 'div[data-cy="select-individualsFiltersBlocks[{}].individualBlockFilters[{}].value"]' + ) + totalNumberOfHouseholdsCount = 'div[data-cy="total-number-of-households-count"]' selectProgramCycleAutocomplete = 'div[data-cy="filters-program-cycle-autocomplete"]' programmeCycleInput = 'div[data-cy="Programme Cycle-input"]' @@ -197,6 +226,93 @@ def getSelectMany(self) -> WebElement: def getButtonEdit(self) -> WebElement: return self.wait_for(self.buttonEdit) + def getTextField(self) -> WebElement: + return self.wait_for(self.textField) + + def getBoolField(self) -> WebElement: + return self.wait_for(self.boolField) + + def getDatePickerFilter(self) -> WebElement: + return self.wait_for(self.datePickerFilter) + + def getSelectIndividualsiFltersBlocksRoundNumber( + self, individuals_filters_blocks_number: int = 0, individual_block_filters_number: int = 0 + ) -> WebElement: + return self.wait_for( + self.selectIndividualsFiltersBlocksRoundNumber.format( + individuals_filters_blocks_number, individual_block_filters_number + ) + ) + + def getSelectRoundOption(self, round_number: int = 0) -> WebElement: + return self.wait_for(self.selectRoundOption.format(round_number)) + + def getSelectIndividualsiFltersBlocksIsNull( + self, individuals_filters_blocks_number: int = 0, individual_block_filters_number: int = 0 + ) -> WebElement: + return self.wait_for( + self.selectIndividualsFiltersBlocksIsNull.format( + individuals_filters_blocks_number, individual_block_filters_number + ) + ) + + def getInputIndividualsFiltersBlocksValueFrom( + self, individuals_filters_blocks_number: int = 0, individual_block_filters_number: int = 0 + ) -> WebElement: + return self.wait_for( + self.inputIndividualsFiltersBlocksValueFrom.format( + individuals_filters_blocks_number, individual_block_filters_number + ) + ) + + def getInputIndividualsFiltersBlocksValueTo( + self, individuals_filters_blocks_number: int = 0, individual_block_filters_number: int = 0 + ) -> WebElement: + return self.wait_for( + self.inputIndividualsFiltersBlocksValueTo.format( + individuals_filters_blocks_number, individual_block_filters_number + ) + ) + + def getInputDateIndividualsFiltersBlocksValueFrom( + self, individuals_filters_blocks_number: int = 0, individual_block_filters_number: int = 0 + ) -> WebElement: + return self.wait_for( + self.inputDateIndividualsFiltersBlocksValueFrom.format( + individuals_filters_blocks_number, individual_block_filters_number + ) + ) + + def getInputDateIndividualsFiltersBlocksValueTo( + self, individuals_filters_blocks_number: int = 0, individual_block_filters_number: int = 0 + ) -> WebElement: + return self.wait_for( + self.inputDateIndividualsFiltersBlocksValueTo.format( + individuals_filters_blocks_number, individual_block_filters_number + ) + ) + + def getInputIndividualsFiltersBlocksValue( + self, individuals_filters_blocks_number: int = 0, individual_block_filters_number: int = 0 + ) -> WebElement: + return self.wait_for( + self.inputIndividualsFiltersBlocksValue.format( + individuals_filters_blocks_number, individual_block_filters_number + ) + ) + + def getSelectIndividualsFiltersBlocksValue( + self, individuals_filters_blocks_number: int = 0, individual_block_filters_number: int = 0 + ) -> WebElement: + return self.wait_for( + self.selectIndividualsFiltersBlocksValue.format( + individuals_filters_blocks_number, individual_block_filters_number + ) + ) + + def getTotalNumberOfHouseholdsCount(self) -> WebElement: + return self.wait_for(self.totalNumberOfHouseholdsCount) + def getFiltersProgramCycleAutocomplete(self) -> WebElement: return self.wait_for(self.selectProgramCycleAutocomplete) diff --git a/backend/selenium_tests/targeting/test_targeting.py b/backend/selenium_tests/targeting/test_targeting.py index 766b41e19f..57d6af982b 100644 --- a/backend/selenium_tests/targeting/test_targeting.py +++ b/backend/selenium_tests/targeting/test_targeting.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Callable from uuid import UUID from django.conf import settings @@ -14,15 +15,32 @@ from selenium.webdriver.common.by import By from hct_mis_api.apps.account.models import User -from hct_mis_api.apps.core.fixtures import DataCollectingTypeFactory -from hct_mis_api.apps.core.models import BusinessArea, DataCollectingType +from hct_mis_api.apps.core.fixtures import DataCollectingTypeFactory, create_afghanistan +from hct_mis_api.apps.core.models import ( + BusinessArea, + DataCollectingType, + FlexibleAttribute, + PeriodicFieldData, +) from hct_mis_api.apps.household.fixtures import ( create_household, create_household_and_individuals, ) -from hct_mis_api.apps.household.models import HEARING, HOST, REFUGEE, SEEING, Household +from hct_mis_api.apps.household.models import ( + HEARING, + HOST, + REFUGEE, + SEEING, + Household, + Individual, +) +from hct_mis_api.apps.periodic_data_update.utils import ( + field_label_to_field_name, + populate_pdu_with_null_values, +) from hct_mis_api.apps.program.fixtures import ProgramFactory from hct_mis_api.apps.program.models import Program +from hct_mis_api.apps.registration_data.fixtures import RegistrationDataImportFactory from hct_mis_api.apps.targeting.fixtures import TargetingCriteriaFactory from hct_mis_api.apps.targeting.models import TargetPopulation from selenium_tests.page_object.filters import Filters @@ -44,6 +62,101 @@ def non_sw_program() -> Program: ) +@pytest.fixture +def program() -> Program: + business_area = create_afghanistan() + return ProgramFactory(name="Test Program", status=Program.ACTIVE, business_area=business_area) + + +@pytest.fixture +def individual(program: Program) -> Callable: + def _individual() -> Individual: + business_area = create_afghanistan() + rdi = RegistrationDataImportFactory() + household, individuals = create_household_and_individuals( + household_data={ + "business_area": business_area, + "program_id": program.pk, + "registration_data_import": rdi, + }, + individuals_data=[ + { + "business_area": business_area, + "program_id": program.pk, + "registration_data_import": rdi, + }, + ], + ) + individual = individuals[0] + individual.flex_fields = populate_pdu_with_null_values(program, individual.flex_fields) + individual.save() + return individual + + return _individual + + +@pytest.fixture +def string_attribute(program: Program) -> FlexibleAttribute: + return create_flexible_attribute( + label="Test String Attribute", + subtype=PeriodicFieldData.STRING, + number_of_rounds=1, + rounds_names=["Test Round String 1"], + program=program, + ) + + +@pytest.fixture +def date_attribute(program: Program) -> FlexibleAttribute: + return create_flexible_attribute( + label="Test Date Attribute", + subtype=PeriodicFieldData.DATE, + number_of_rounds=1, + rounds_names=["Test Round Date 1"], + program=program, + ) + + +@pytest.fixture +def bool_attribute(program: Program) -> FlexibleAttribute: + return create_flexible_attribute( + label="Test Bool Attribute", + subtype=PeriodicFieldData.BOOL, + number_of_rounds=2, + rounds_names=["Test Round Bool 1", "Test Round Bool 2"], + program=program, + ) + + +@pytest.fixture +def decimal_attribute(program: Program) -> FlexibleAttribute: + return create_flexible_attribute( + label="Test Decimal Attribute", + subtype=PeriodicFieldData.DECIMAL, + number_of_rounds=1, + rounds_names=["Test Round Decimal 1"], + program=program, + ) + + +def create_flexible_attribute( + label: str, subtype: str, number_of_rounds: int, rounds_names: list[str], program: Program +) -> FlexibleAttribute: + name = field_label_to_field_name(label) + flexible_attribute = FlexibleAttribute.objects.create( + label={"English(EN)": label}, + name=name, + type=FlexibleAttribute.PDU, + associated_with=FlexibleAttribute.ASSOCIATED_WITH_INDIVIDUAL, + program=program, + ) + flexible_attribute.pdu_data = PeriodicFieldData.objects.create( + subtype=subtype, number_of_rounds=number_of_rounds, rounds_names=rounds_names + ) + flexible_attribute.save() + return flexible_attribute + + def create_custom_household(observed_disability: list[str], residence_status: str = HOST) -> Household: program = Program.objects.get(name="Test Programm") household, _ = create_household_and_individuals( @@ -298,6 +411,7 @@ def test_create_targeting_for_normal_program( pageTargetingCreate.getTargetingCriteriaValue().click() pageTargetingCreate.select_option_by_name(REFUGEE) pageTargetingCreate.getTargetingCriteriaAddDialogSaveButton().click() + disability_expected_criteria_text = "Residence status: Displaced | Refugee / Asylum Seeker" assert pageTargetingCreate.getCriteriaContainer().text == disability_expected_criteria_text targeting_name = "Test targeting people" @@ -313,6 +427,267 @@ def test_create_targeting_for_normal_program( actions.move_to_element(pageTargetingDetails.getHouseholdTableCell(1, 1)).perform() # type: ignore assert len(pageTargetingDetails.getHouseholdTableRows()) == 1 + def test_create_targeting_with_pdu_string_criteria( + self, + program: Program, + pageTargeting: Targeting, + pageTargetingCreate: TargetingCreate, + pageTargetingDetails: TargetingDetails, + individual: Callable, + string_attribute: FlexibleAttribute, + ) -> None: + individual1 = individual() + individual1.flex_fields[string_attribute.name]["1"]["value"] = "Text" + individual1.save() + individual2 = individual() + individual2.flex_fields[string_attribute.name]["1"]["value"] = "Test" + individual2.save() + individual() + pageTargeting.navigate_to_page("afghanistan", program.id) + pageTargeting.getButtonCreateNew().click() + pageTargeting.getButtonCreateNewByFilters().click() + assert "New Target Population" in pageTargetingCreate.getTitlePage().text + pageTargetingCreate.getAddCriteriaButton().click() + pageTargetingCreate.getAddIndividualRuleButton().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys("Test String Attribute") + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ARROW_DOWN) + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ENTER) + pageTargetingCreate.getSelectIndividualsiFltersBlocksRoundNumber().click() + pageTargetingCreate.getSelectRoundOption(1).click() + pageTargetingCreate.getInputIndividualsFiltersBlocksValue().send_keys("Text") + pageTargetingCreate.getTargetingCriteriaAddDialogSaveButton().click() + expected_criteria_text = "Test String Attribute: Text\nRound 1 (Test Round String 1)" + assert pageTargetingCreate.getCriteriaContainer().text == expected_criteria_text + targeting_name = "Test Targeting PDU string" + pageTargetingCreate.getFieldName().send_keys(targeting_name) + pageTargetingCreate.getTargetPopulationSaveButton().click() + pageTargetingDetails.getLockButton() + assert pageTargetingDetails.getTitlePage().text == targeting_name + assert pageTargetingDetails.getCriteriaContainer().text == expected_criteria_text + assert Household.objects.count() == 3 + assert pageTargetingDetails.getHouseholdTableCell(1, 1).text == individual1.household.unicef_id + assert pageTargetingCreate.getTotalNumberOfHouseholdsCount().text == "1" + assert len(pageTargetingDetails.getHouseholdTableRows()) == 1 + + def test_create_targeting_with_pdu_bool_criteria( + self, + program: Program, + pageTargeting: Targeting, + pageTargetingCreate: TargetingCreate, + pageTargetingDetails: TargetingDetails, + individual: Callable, + bool_attribute: FlexibleAttribute, + ) -> None: + individual1 = individual() + individual1.flex_fields[bool_attribute.name]["2"]["value"] = True + individual1.save() + individual2 = individual() + individual2.flex_fields[bool_attribute.name]["2"]["value"] = False + individual2.save() + individual() + pageTargeting.navigate_to_page("afghanistan", program.id) + pageTargeting.getButtonCreateNew().click() + pageTargeting.getButtonCreateNewByFilters().click() + assert "New Target Population" in pageTargetingCreate.getTitlePage().text + pageTargetingCreate.getAddCriteriaButton().click() + pageTargetingCreate.getAddIndividualRuleButton().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys("Test Bool Attribute") + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ARROW_DOWN) + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ENTER) + pageTargetingCreate.getSelectIndividualsiFltersBlocksRoundNumber().click() + pageTargetingCreate.getSelectRoundOption(2).click() + pageTargetingCreate.getSelectIndividualsFiltersBlocksValue().click() + pageTargetingCreate.select_option_by_name("Yes") + pageTargetingCreate.getTargetingCriteriaAddDialogSaveButton().click() + bool_yes_expected_criteria_text = "Test Bool Attribute: Yes\nRound 2 (Test Round Bool 2)" + assert pageTargetingCreate.getCriteriaContainer().text == bool_yes_expected_criteria_text + + targeting_name = "Test Targeting PDU bool" + pageTargetingCreate.getFieldName().send_keys(targeting_name) + pageTargetingCreate.getTargetPopulationSaveButton().click() + + pageTargetingDetails.getLockButton() + + assert pageTargetingDetails.getTitlePage().text == targeting_name + assert pageTargetingDetails.getCriteriaContainer().text == bool_yes_expected_criteria_text + assert Household.objects.count() == 3 + assert pageTargetingDetails.getHouseholdTableCell(1, 1).text == individual1.household.unicef_id + assert pageTargetingCreate.getTotalNumberOfHouseholdsCount().text == "1" + assert len(pageTargetingDetails.getHouseholdTableRows()) == 1 + + # edit to False + pageTargetingDetails.getButtonEdit().click() + pageTargetingDetails.getButtonIconEdit().click() + pageTargetingCreate.getSelectIndividualsFiltersBlocksValue().click() + pageTargetingCreate.select_option_by_name("No") + bool_no_expected_criteria_text = "Test Bool Attribute: No\nRound 2 (Test Round Bool 2)" + + pageTargetingCreate.get_elements(pageTargetingCreate.targetingCriteriaAddDialogSaveButton)[1].click() + + assert pageTargetingCreate.getCriteriaContainer().text == bool_no_expected_criteria_text + pageTargetingCreate.getButtonSave().click() + pageTargetingDetails.getLockButton() + + assert pageTargetingDetails.getCriteriaContainer().text == bool_no_expected_criteria_text + assert pageTargetingDetails.getHouseholdTableCell(1, 1).text == individual2.household.unicef_id + assert pageTargetingCreate.getTotalNumberOfHouseholdsCount().text == "1" + assert len(pageTargetingDetails.getHouseholdTableRows()) == 1 + + def test_create_targeting_with_pdu_decimal_criteria( + self, + program: Program, + pageTargeting: Targeting, + pageTargetingCreate: TargetingCreate, + pageTargetingDetails: TargetingDetails, + individual: Callable, + decimal_attribute: FlexibleAttribute, + ) -> None: + individual1 = individual() + individual1.flex_fields[decimal_attribute.name]["1"]["value"] = 2.5 + individual1.save() + individual2 = individual() + individual2.flex_fields[decimal_attribute.name]["1"]["value"] = 5.0 + individual2.save() + individual() + pageTargeting.navigate_to_page("afghanistan", program.id) + pageTargeting.getButtonCreateNew().click() + pageTargeting.getButtonCreateNewByFilters().click() + assert "New Target Population" in pageTargetingCreate.getTitlePage().text + pageTargetingCreate.getAddCriteriaButton().click() + pageTargetingCreate.getAddIndividualRuleButton().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys("Test Decimal Attribute") + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ARROW_DOWN) + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ENTER) + pageTargetingCreate.getSelectIndividualsiFltersBlocksRoundNumber().click() + pageTargetingCreate.getSelectRoundOption(1).click() + pageTargetingCreate.getInputIndividualsFiltersBlocksValueFrom().send_keys("2") + pageTargetingCreate.getInputIndividualsFiltersBlocksValueTo().send_keys("4") + pageTargetingCreate.getTargetingCriteriaAddDialogSaveButton().click() + expected_criteria_text = "Test Decimal Attribute: 2 - 4\nRound 1 (Test Round Decimal 1)" + assert pageTargetingCreate.getCriteriaContainer().text == expected_criteria_text + targeting_name = "Test Targeting PDU decimal" + pageTargetingCreate.getFieldName().send_keys(targeting_name) + pageTargetingCreate.getTargetPopulationSaveButton().click() + pageTargetingDetails.getLockButton() + assert pageTargetingDetails.getTitlePage().text == targeting_name + assert pageTargetingDetails.getCriteriaContainer().text == expected_criteria_text + assert Household.objects.count() == 3 + assert pageTargetingDetails.getHouseholdTableCell(1, 1).text == individual1.household.unicef_id + assert pageTargetingCreate.getTotalNumberOfHouseholdsCount().text == "1" + assert len(pageTargetingDetails.getHouseholdTableRows()) == 1 + + # edit range + pageTargetingDetails.getButtonEdit().click() + pageTargetingDetails.getButtonIconEdit().click() + pageTargetingCreate.getInputIndividualsFiltersBlocksValueTo().send_keys(Keys.BACKSPACE) + pageTargetingCreate.getInputIndividualsFiltersBlocksValueTo().send_keys("5") + bool_no_expected_criteria_text = "Test Decimal Attribute: 2 - 5\nRound 1 (Test Round Decimal 1)" + + pageTargetingCreate.get_elements(pageTargetingCreate.targetingCriteriaAddDialogSaveButton)[1].click() + + assert pageTargetingCreate.getCriteriaContainer().text == bool_no_expected_criteria_text + pageTargetingCreate.getButtonSave().click() + pageTargetingDetails.getLockButton() + + assert pageTargetingDetails.getCriteriaContainer().text == bool_no_expected_criteria_text + assert pageTargetingDetails.getHouseholdTableCell(1, 1).text == individual1.household.unicef_id + assert pageTargetingDetails.getHouseholdTableCell(2, 1).text == individual2.household.unicef_id + assert pageTargetingCreate.getTotalNumberOfHouseholdsCount().text == "2" + assert len(pageTargetingDetails.getHouseholdTableRows()) == 2 + + def test_create_targeting_with_pdu_date_criteria( + self, + program: Program, + pageTargeting: Targeting, + pageTargetingCreate: TargetingCreate, + pageTargetingDetails: TargetingDetails, + individual: Callable, + date_attribute: FlexibleAttribute, + ) -> None: + individual1 = individual() + individual1.flex_fields[date_attribute.name]["1"]["value"] = "2022-02-02" + individual1.save() + individual2 = individual() + individual2.flex_fields[date_attribute.name]["1"]["value"] = "2022-10-02" + individual2.save() + individual() + pageTargeting.navigate_to_page("afghanistan", program.id) + pageTargeting.getButtonCreateNew().click() + pageTargeting.getButtonCreateNewByFilters().click() + assert "New Target Population" in pageTargetingCreate.getTitlePage().text + pageTargetingCreate.getAddCriteriaButton().click() + pageTargetingCreate.getAddIndividualRuleButton().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys("Test Date Attribute") + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ARROW_DOWN) + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ENTER) + pageTargetingCreate.getSelectIndividualsiFltersBlocksRoundNumber().click() + pageTargetingCreate.getSelectRoundOption(1).click() + pageTargetingCreate.getInputDateIndividualsFiltersBlocksValueFrom().click() + pageTargetingCreate.getInputDateIndividualsFiltersBlocksValueFrom().send_keys("2022-01-01") + pageTargetingCreate.getInputDateIndividualsFiltersBlocksValueTo().click() + pageTargetingCreate.getInputDateIndividualsFiltersBlocksValueTo().send_keys("2022-03-03") + pageTargetingCreate.getTargetingCriteriaAddDialogSaveButton().click() + expected_criteria_text = "Test Date Attribute: 2022-01-01 - 2022-03-03\nRound 1 (Test Round Date 1)" + assert pageTargetingCreate.getCriteriaContainer().text == expected_criteria_text + targeting_name = "Test Targeting PDU date" + pageTargetingCreate.getFieldName().send_keys(targeting_name) + pageTargetingCreate.getTargetPopulationSaveButton().click() + pageTargetingDetails.getLockButton() + assert pageTargetingDetails.getTitlePage().text == targeting_name + assert pageTargetingDetails.getCriteriaContainer().text == expected_criteria_text + assert Household.objects.count() == 3 + assert pageTargetingDetails.getHouseholdTableCell(1, 1).text == individual1.household.unicef_id + assert pageTargetingCreate.getTotalNumberOfHouseholdsCount().text == "1" + assert len(pageTargetingDetails.getHouseholdTableRows()) == 1 + + def test_create_targeting_with_pdu_null_criteria( + self, + program: Program, + pageTargeting: Targeting, + pageTargetingCreate: TargetingCreate, + pageTargetingDetails: TargetingDetails, + individual: Callable, + string_attribute: FlexibleAttribute, + ) -> None: + individual1 = individual() + individual1.flex_fields[string_attribute.name]["1"]["value"] = "Text" + individual1.save() + individual2 = individual() + individual2.flex_fields[string_attribute.name]["1"]["value"] = "Test" + individual2.save() + individual3 = individual() + pageTargeting.navigate_to_page("afghanistan", program.id) + pageTargeting.getButtonCreateNew().click() + pageTargeting.getButtonCreateNewByFilters().click() + assert "New Target Population" in pageTargetingCreate.getTitlePage().text + pageTargetingCreate.getAddCriteriaButton().click() + pageTargetingCreate.getAddIndividualRuleButton().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().click() + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys("Test String Attribute") + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ARROW_DOWN) + pageTargetingCreate.getTargetingCriteriaAutoComplete().send_keys(Keys.ENTER) + pageTargetingCreate.getSelectIndividualsiFltersBlocksRoundNumber().click() + pageTargetingCreate.getSelectRoundOption(1).click() + pageTargetingCreate.getSelectIndividualsiFltersBlocksIsNull().click() + pageTargetingCreate.getTargetingCriteriaAddDialogSaveButton().click() + expected_criteria_text = "Test String Attribute: Empty\nRound 1 (Test Round String 1)" + assert pageTargetingCreate.getCriteriaContainer().text == expected_criteria_text + targeting_name = "Test Targeting PDU null" + pageTargetingCreate.getFieldName().send_keys(targeting_name) + pageTargetingCreate.getTargetPopulationSaveButton().click() + pageTargetingDetails.getLockButton() + assert pageTargetingDetails.getTitlePage().text == targeting_name + assert pageTargetingDetails.getCriteriaContainer().text == expected_criteria_text + assert Household.objects.count() == 3 + + assert pageTargetingDetails.getHouseholdTableCell(1, 1).text == individual3.household.unicef_id + assert pageTargetingCreate.getTotalNumberOfHouseholdsCount().text == "1" + assert len(pageTargetingDetails.getHouseholdTableRows()) == 1 + @pytest.mark.night @pytest.mark.usefixtures("login") diff --git a/frontend/data/schema.graphql b/frontend/data/schema.graphql index e2d83d00e8..224047abe4 100644 --- a/frontend/data/schema.graphql +++ b/frontend/data/schema.graphql @@ -1362,6 +1362,12 @@ type FinishPaymentVerificationPlan { paymentPlan: GenericPaymentPlanNode } +enum FlexFieldClassificationChoices { + NOT_FLEX_FIELD + FLEX_FIELD_BASIC + FLEX_FIELD_PDU +} + scalar FlexFieldsScalar type FspChoice { @@ -3650,7 +3656,7 @@ enum PeriodicFieldDataSubtype { DATE DECIMAL STRING - BOOLEAN + BOOL } type PeriodicFieldNode implements Node { @@ -3760,6 +3766,7 @@ type ProgramNode implements Node { totalNumberOfHouseholds: Int totalNumberOfHouseholdsWithTpInProgram: Int isSocialWorkerProgram: Boolean + targetPopulationsCount: Int } type ProgramNodeConnection { @@ -4738,6 +4745,13 @@ enum TargetingCriteriaRuleFilterComparisonMethod { NOT_IN_RANGE GREATER_THAN LESS_THAN + IS_NULL +} + +enum TargetingCriteriaRuleFilterFlexFieldClassification { + NOT_FLEX_FIELD + FLEX_FIELD_BASIC + FLEX_FIELD_PDU } type TargetingCriteriaRuleFilterNode { @@ -4746,7 +4760,7 @@ type TargetingCriteriaRuleFilterNode { updatedAt: DateTime! comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod! targetingCriteriaRule: TargetingCriteriaRuleNode! - isFlexField: Boolean! + flexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification! fieldName: String! arguments: [Arg] fieldAttribute: FieldAttributeNode @@ -4754,9 +4768,10 @@ type TargetingCriteriaRuleFilterNode { input TargetingCriteriaRuleFilterObjectType { comparisonMethod: String! - isFlexField: Boolean! + flexFieldClassification: FlexFieldClassificationChoices! fieldName: String! arguments: [Arg]! + roundNumber: Int } type TargetingCriteriaRuleNode { @@ -4782,6 +4797,13 @@ enum TargetingIndividualBlockRuleFilterComparisonMethod { NOT_IN_RANGE GREATER_THAN LESS_THAN + IS_NULL +} + +enum TargetingIndividualBlockRuleFilterFlexFieldClassification { + NOT_FLEX_FIELD + FLEX_FIELD_BASIC + FLEX_FIELD_PDU } type TargetingIndividualBlockRuleFilterNode { @@ -4790,9 +4812,10 @@ type TargetingIndividualBlockRuleFilterNode { updatedAt: DateTime! comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod! individualsFiltersBlock: TargetingIndividualRuleFilterBlockNode! - isFlexField: Boolean! + flexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification! fieldName: String! arguments: [Arg] + roundNumber: Int fieldAttribute: FieldAttributeNode } diff --git a/frontend/src/__generated__/graphql.tsx b/frontend/src/__generated__/graphql.tsx index 7c62a1fc93..e4426548b0 100644 --- a/frontend/src/__generated__/graphql.tsx +++ b/frontend/src/__generated__/graphql.tsx @@ -2016,6 +2016,12 @@ export type FinishPaymentVerificationPlan = { paymentPlan?: Maybe; }; +export enum FlexFieldClassificationChoices { + FlexFieldBasic = 'FLEX_FIELD_BASIC', + FlexFieldPdu = 'FLEX_FIELD_PDU', + NotFlexField = 'NOT_FLEX_FIELD' +} + export type FspChoice = { __typename?: 'FspChoice'; configurations?: Maybe>>; @@ -5570,7 +5576,7 @@ export type PeriodicFieldDataNode = { }; export enum PeriodicFieldDataSubtype { - Boolean = 'BOOLEAN', + Bool = 'BOOL', Date = 'DATE', Decimal = 'DECIMAL', String = 'STRING' @@ -5721,6 +5727,7 @@ export type ProgramNode = Node & { startDate: Scalars['Date']['output']; status: ProgramStatus; surveys: SurveyNodeConnection; + targetPopulationsCount?: Maybe; targetpopulationSet: TargetPopulationNodeConnection; totalDeliveredQuantity?: Maybe; totalEntitledQuantity?: Maybe; @@ -8292,6 +8299,7 @@ export enum TargetingCriteriaRuleFilterComparisonMethod { Contains = 'CONTAINS', Equals = 'EQUALS', GreaterThan = 'GREATER_THAN', + IsNull = 'IS_NULL', LessThan = 'LESS_THAN', NotContains = 'NOT_CONTAINS', NotEquals = 'NOT_EQUALS', @@ -8299,6 +8307,12 @@ export enum TargetingCriteriaRuleFilterComparisonMethod { Range = 'RANGE' } +export enum TargetingCriteriaRuleFilterFlexFieldClassification { + FlexFieldBasic = 'FLEX_FIELD_BASIC', + FlexFieldPdu = 'FLEX_FIELD_PDU', + NotFlexField = 'NOT_FLEX_FIELD' +} + export type TargetingCriteriaRuleFilterNode = { __typename?: 'TargetingCriteriaRuleFilterNode'; arguments?: Maybe>>; @@ -8306,8 +8320,8 @@ export type TargetingCriteriaRuleFilterNode = { createdAt: Scalars['DateTime']['output']; fieldAttribute?: Maybe; fieldName: Scalars['String']['output']; + flexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification; id: Scalars['UUID']['output']; - isFlexField: Scalars['Boolean']['output']; targetingCriteriaRule: TargetingCriteriaRuleNode; updatedAt: Scalars['DateTime']['output']; }; @@ -8316,7 +8330,8 @@ export type TargetingCriteriaRuleFilterObjectType = { arguments: Array>; comparisonMethod: Scalars['String']['input']; fieldName: Scalars['String']['input']; - isFlexField: Scalars['Boolean']['input']; + flexFieldClassification: FlexFieldClassificationChoices; + roundNumber?: InputMaybe; }; export type TargetingCriteriaRuleNode = { @@ -8338,6 +8353,7 @@ export enum TargetingIndividualBlockRuleFilterComparisonMethod { Contains = 'CONTAINS', Equals = 'EQUALS', GreaterThan = 'GREATER_THAN', + IsNull = 'IS_NULL', LessThan = 'LESS_THAN', NotContains = 'NOT_CONTAINS', NotEquals = 'NOT_EQUALS', @@ -8345,6 +8361,12 @@ export enum TargetingIndividualBlockRuleFilterComparisonMethod { Range = 'RANGE' } +export enum TargetingIndividualBlockRuleFilterFlexFieldClassification { + FlexFieldBasic = 'FLEX_FIELD_BASIC', + FlexFieldPdu = 'FLEX_FIELD_PDU', + NotFlexField = 'NOT_FLEX_FIELD' +} + export type TargetingIndividualBlockRuleFilterNode = { __typename?: 'TargetingIndividualBlockRuleFilterNode'; arguments?: Maybe>>; @@ -8352,9 +8374,10 @@ export type TargetingIndividualBlockRuleFilterNode = { createdAt: Scalars['DateTime']['output']; fieldAttribute?: Maybe; fieldName: Scalars['String']['output']; + flexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification; id: Scalars['UUID']['output']; individualsFiltersBlock: TargetingIndividualRuleFilterBlockNode; - isFlexField: Scalars['Boolean']['output']; + roundNumber?: Maybe; updatedAt: Scalars['DateTime']['output']; }; @@ -9595,7 +9618,7 @@ export type MergedIndividualMinimalFragment = { __typename?: 'IndividualNode', i export type PaymentRecordDetailsFragment = { __typename?: 'PaymentRecordNode', id: string, status: PaymentRecordStatus, statusDate: any, caId?: string | null, caHashId?: any | null, registrationCaId?: string | null, fullName: string, distributionModality: string, totalPersonsCovered: number, currency: string, entitlementQuantity?: number | null, deliveredQuantity?: number | null, deliveredQuantityUsd?: number | null, deliveryDate?: any | null, entitlementCardIssueDate?: any | null, entitlementCardNumber?: string | null, transactionReferenceId?: string | null, verification?: { __typename?: 'PaymentVerificationNode', id: string, status: PaymentVerificationStatus, statusDate?: any | null, receivedAmount?: number | null } | null, household: { __typename?: 'HouseholdNode', id: string, status?: string | null, size?: number | null, unicefId?: string | null, headOfHousehold?: { __typename?: 'IndividualNode', id: string, phoneNo: string, phoneNoAlternative: string, phoneNoValid?: boolean | null, phoneNoAlternativeValid?: boolean | null } | null }, targetPopulation: { __typename?: 'TargetPopulationNode', id: string, name: string }, parent?: { __typename?: 'CashPlanNode', id: string, caId?: string | null, program: { __typename?: 'ProgramNode', id: string, name: string }, verificationPlans?: { __typename?: 'PaymentVerificationPlanNodeConnection', edges: Array<{ __typename?: 'PaymentVerificationPlanNodeEdge', node?: { __typename?: 'PaymentVerificationPlanNode', id: string, status: PaymentVerificationPlanStatus, verificationChannel: PaymentVerificationPlanVerificationChannel } | null } | null> } | null } | null, deliveryType?: { __typename?: 'DeliveryMechanismNode', name?: string | null } | null, serviceProvider: { __typename?: 'ServiceProviderNode', id: string, fullName?: string | null, shortName?: string | null } }; -export type ProgramDetailsFragment = { __typename?: 'ProgramNode', id: string, name: string, programmeCode?: string | null, startDate: any, endDate: any, status: ProgramStatus, caId?: string | null, caHashId?: string | null, description: string, budget?: any | null, frequencyOfPayments: ProgramFrequencyOfPayments, cashPlus: boolean, populationGoal: number, scope?: ProgramScope | null, sector: ProgramSector, totalNumberOfHouseholds?: number | null, totalNumberOfHouseholdsWithTpInProgram?: number | null, administrativeAreasOfImplementation: string, isSocialWorkerProgram?: boolean | null, version: any, adminUrl?: string | null, partnerAccess: ProgramPartnerAccess, dataCollectingType?: { __typename?: 'DataCollectingTypeNode', id: string, code: string, label: string, active: boolean, individualFiltersAvailable: boolean, householdFiltersAvailable: boolean, description: string, type?: DataCollectingTypeType | null } | null, partners?: Array<{ __typename?: 'PartnerNode', id: string, name?: string | null, areaAccess?: string | null, areas?: Array<{ __typename?: 'AreaNode', id: string, level: number } | null> | null } | null> | null, registrationImports: { __typename?: 'RegistrationDataImportNodeConnection', totalCount?: number | null }, pduFields?: Array<{ __typename?: 'PeriodicFieldNode', id: string, label: any, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null> | null }; +export type ProgramDetailsFragment = { __typename?: 'ProgramNode', id: string, name: string, programmeCode?: string | null, startDate: any, endDate: any, status: ProgramStatus, caId?: string | null, caHashId?: string | null, description: string, budget?: any | null, frequencyOfPayments: ProgramFrequencyOfPayments, cashPlus: boolean, populationGoal: number, scope?: ProgramScope | null, sector: ProgramSector, totalNumberOfHouseholds?: number | null, totalNumberOfHouseholdsWithTpInProgram?: number | null, administrativeAreasOfImplementation: string, isSocialWorkerProgram?: boolean | null, version: any, adminUrl?: string | null, partnerAccess: ProgramPartnerAccess, targetPopulationsCount?: number | null, dataCollectingType?: { __typename?: 'DataCollectingTypeNode', id: string, code: string, label: string, active: boolean, individualFiltersAvailable: boolean, householdFiltersAvailable: boolean, description: string, type?: DataCollectingTypeType | null } | null, partners?: Array<{ __typename?: 'PartnerNode', id: string, name?: string | null, areaAccess?: string | null, areas?: Array<{ __typename?: 'AreaNode', id: string, level: number } | null> | null } | null> | null, registrationImports: { __typename?: 'RegistrationDataImportNodeConnection', totalCount?: number | null }, pduFields?: Array<{ __typename?: 'PeriodicFieldNode', id: string, label: any, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null> | null }; export type RegistrationMinimalFragment = { __typename?: 'RegistrationDataImportNode', id: string, createdAt: any, name: string, status: RegistrationDataImportStatus, erased: boolean, importDate: any, dataSource: RegistrationDataImportDataSource, numberOfHouseholds: number, numberOfIndividuals: number, refuseReason?: string | null, totalHouseholdsCountWithValidPhoneNo?: number | null, adminUrl?: string | null, importedBy?: { __typename?: 'UserNode', id: string, firstName: string, lastName: string, email: string } | null, program?: { __typename?: 'ProgramNode', id: string, name: string, startDate: any, endDate: any, status: ProgramStatus } | null }; @@ -9611,7 +9634,7 @@ export type ImportedIndividualDetailedFragment = { __typename?: 'ImportedIndivid export type TargetPopulationMinimalFragment = { __typename: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, createdAt: any, updatedAt: any, totalHouseholdsCount?: number | null, totalHouseholdsCountWithValidPhoneNo?: number | null, totalIndividualsCount?: number | null, program?: { __typename: 'ProgramNode', id: string, name: string } | null, createdBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null }; -export type TargetPopulationDetailedFragment = { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null } | null }; +export type TargetPopulationDetailedFragment = { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification, roundNumber?: number | null, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null } | null }; export type CreateFeedbackTicketMutationVariables = Exact<{ input: CreateFeedbackInput; @@ -10076,7 +10099,7 @@ export type UpdateProgramMutationVariables = Exact<{ }>; -export type UpdateProgramMutation = { __typename?: 'Mutations', updateProgram?: { __typename?: 'UpdateProgram', validationErrors?: any | null, program?: { __typename?: 'ProgramNode', id: string, name: string, programmeCode?: string | null, startDate: any, endDate: any, status: ProgramStatus, caId?: string | null, caHashId?: string | null, description: string, budget?: any | null, frequencyOfPayments: ProgramFrequencyOfPayments, cashPlus: boolean, populationGoal: number, scope?: ProgramScope | null, sector: ProgramSector, totalNumberOfHouseholds?: number | null, totalNumberOfHouseholdsWithTpInProgram?: number | null, administrativeAreasOfImplementation: string, isSocialWorkerProgram?: boolean | null, version: any, adminUrl?: string | null, partnerAccess: ProgramPartnerAccess, dataCollectingType?: { __typename?: 'DataCollectingTypeNode', id: string, code: string, label: string, active: boolean, individualFiltersAvailable: boolean, householdFiltersAvailable: boolean, description: string, type?: DataCollectingTypeType | null } | null, partners?: Array<{ __typename?: 'PartnerNode', id: string, name?: string | null, areaAccess?: string | null, areas?: Array<{ __typename?: 'AreaNode', id: string, level: number } | null> | null } | null> | null, registrationImports: { __typename?: 'RegistrationDataImportNodeConnection', totalCount?: number | null }, pduFields?: Array<{ __typename?: 'PeriodicFieldNode', id: string, label: any, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null> | null } | null } | null }; +export type UpdateProgramMutation = { __typename?: 'Mutations', updateProgram?: { __typename?: 'UpdateProgram', validationErrors?: any | null, program?: { __typename?: 'ProgramNode', id: string, name: string, programmeCode?: string | null, startDate: any, endDate: any, status: ProgramStatus, caId?: string | null, caHashId?: string | null, description: string, budget?: any | null, frequencyOfPayments: ProgramFrequencyOfPayments, cashPlus: boolean, populationGoal: number, scope?: ProgramScope | null, sector: ProgramSector, totalNumberOfHouseholds?: number | null, totalNumberOfHouseholdsWithTpInProgram?: number | null, administrativeAreasOfImplementation: string, isSocialWorkerProgram?: boolean | null, version: any, adminUrl?: string | null, partnerAccess: ProgramPartnerAccess, targetPopulationsCount?: number | null, dataCollectingType?: { __typename?: 'DataCollectingTypeNode', id: string, code: string, label: string, active: boolean, individualFiltersAvailable: boolean, householdFiltersAvailable: boolean, description: string, type?: DataCollectingTypeType | null } | null, partners?: Array<{ __typename?: 'PartnerNode', id: string, name?: string | null, areaAccess?: string | null, areas?: Array<{ __typename?: 'AreaNode', id: string, level: number } | null> | null } | null> | null, registrationImports: { __typename?: 'RegistrationDataImportNodeConnection', totalCount?: number | null }, pduFields?: Array<{ __typename?: 'PeriodicFieldNode', id: string, label: any, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null> | null } | null } | null }; export type CreateRegistrationKoboImportMutationVariables = Exact<{ registrationDataImportData: RegistrationKoboImportMutationInput; @@ -10203,35 +10226,35 @@ export type FinalizeTpMutationVariables = Exact<{ }>; -export type FinalizeTpMutation = { __typename?: 'Mutations', finalizeTargetPopulation?: { __typename?: 'FinalizeTargetPopulationMutation', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null } | null } | null } | null }; +export type FinalizeTpMutation = { __typename?: 'Mutations', finalizeTargetPopulation?: { __typename?: 'FinalizeTargetPopulationMutation', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification, roundNumber?: number | null, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null } | null } | null } | null }; export type LockTpMutationVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type LockTpMutation = { __typename?: 'Mutations', lockTargetPopulation?: { __typename?: 'LockTargetPopulationMutation', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null } | null } | null } | null }; +export type LockTpMutation = { __typename?: 'Mutations', lockTargetPopulation?: { __typename?: 'LockTargetPopulationMutation', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification, roundNumber?: number | null, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null } | null } | null } | null }; export type RebuildTpMutationVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type RebuildTpMutation = { __typename?: 'Mutations', targetPopulationRebuild?: { __typename?: 'RebuildTargetPopulationMutation', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null } | null } | null } | null }; +export type RebuildTpMutation = { __typename?: 'Mutations', targetPopulationRebuild?: { __typename?: 'RebuildTargetPopulationMutation', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification, roundNumber?: number | null, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null } | null } | null } | null }; export type SetSteficonRuleOnTargetPopulationMutationVariables = Exact<{ input: SetSteficonRuleOnTargetPopulationMutationInput; }>; -export type SetSteficonRuleOnTargetPopulationMutation = { __typename?: 'Mutations', setSteficonRuleOnTargetPopulation?: { __typename?: 'SetSteficonRuleOnTargetPopulationMutationPayload', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null } | null } | null } | null }; +export type SetSteficonRuleOnTargetPopulationMutation = { __typename?: 'Mutations', setSteficonRuleOnTargetPopulation?: { __typename?: 'SetSteficonRuleOnTargetPopulationMutationPayload', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification, roundNumber?: number | null, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null } | null } | null } | null }; export type UnlockTpMutationVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type UnlockTpMutation = { __typename?: 'Mutations', unlockTargetPopulation?: { __typename?: 'UnlockTargetPopulationMutation', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null } | null } | null } | null }; +export type UnlockTpMutation = { __typename?: 'Mutations', unlockTargetPopulation?: { __typename?: 'UnlockTargetPopulationMutation', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification, roundNumber?: number | null, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null } | null } | null } | null }; export type UpdateTpMutationVariables = Exact<{ input: UpdateTargetPopulationInput; @@ -10464,7 +10487,7 @@ export type ImportedIndividualFieldsQueryVariables = Exact<{ }>; -export type ImportedIndividualFieldsQuery = { __typename?: 'Query', allFieldsAttributes?: Array<{ __typename?: 'FieldAttributeNode', isFlexField?: boolean | null, id?: string | null, type?: string | null, name?: string | null, associatedWith?: string | null, labelEn?: string | null, hint?: string | null, labels?: Array<{ __typename?: 'LabelNode', language?: string | null, label?: string | null } | null> | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', labelEn?: string | null, value?: string | null, admin?: string | null, listName?: string | null, labels?: Array<{ __typename?: 'LabelNode', label?: string | null, language?: string | null } | null> | null } | null> | null } | null> | null }; +export type ImportedIndividualFieldsQuery = { __typename?: 'Query', allFieldsAttributes?: Array<{ __typename?: 'FieldAttributeNode', isFlexField?: boolean | null, id?: string | null, type?: string | null, name?: string | null, associatedWith?: string | null, labelEn?: string | null, hint?: string | null, labels?: Array<{ __typename?: 'LabelNode', language?: string | null, label?: string | null } | null> | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', labelEn?: string | null, value?: string | null, admin?: string | null, listName?: string | null, labels?: Array<{ __typename?: 'LabelNode', label?: string | null, language?: string | null } | null> | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null> | null }; export type PduSubtypeChoicesDataQueryVariables = Exact<{ [key: string]: never; }>; @@ -11067,7 +11090,7 @@ export type ProgramQueryVariables = Exact<{ }>; -export type ProgramQuery = { __typename?: 'Query', program?: { __typename?: 'ProgramNode', id: string, name: string, programmeCode?: string | null, startDate: any, endDate: any, status: ProgramStatus, caId?: string | null, caHashId?: string | null, description: string, budget?: any | null, frequencyOfPayments: ProgramFrequencyOfPayments, cashPlus: boolean, populationGoal: number, scope?: ProgramScope | null, sector: ProgramSector, totalNumberOfHouseholds?: number | null, totalNumberOfHouseholdsWithTpInProgram?: number | null, administrativeAreasOfImplementation: string, isSocialWorkerProgram?: boolean | null, version: any, adminUrl?: string | null, partnerAccess: ProgramPartnerAccess, dataCollectingType?: { __typename?: 'DataCollectingTypeNode', id: string, code: string, label: string, active: boolean, individualFiltersAvailable: boolean, householdFiltersAvailable: boolean, description: string, type?: DataCollectingTypeType | null } | null, partners?: Array<{ __typename?: 'PartnerNode', id: string, name?: string | null, areaAccess?: string | null, areas?: Array<{ __typename?: 'AreaNode', id: string, level: number } | null> | null } | null> | null, registrationImports: { __typename?: 'RegistrationDataImportNodeConnection', totalCount?: number | null }, pduFields?: Array<{ __typename?: 'PeriodicFieldNode', id: string, label: any, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null> | null } | null }; +export type ProgramQuery = { __typename?: 'Query', program?: { __typename?: 'ProgramNode', id: string, name: string, programmeCode?: string | null, startDate: any, endDate: any, status: ProgramStatus, caId?: string | null, caHashId?: string | null, description: string, budget?: any | null, frequencyOfPayments: ProgramFrequencyOfPayments, cashPlus: boolean, populationGoal: number, scope?: ProgramScope | null, sector: ProgramSector, totalNumberOfHouseholds?: number | null, totalNumberOfHouseholdsWithTpInProgram?: number | null, administrativeAreasOfImplementation: string, isSocialWorkerProgram?: boolean | null, version: any, adminUrl?: string | null, partnerAccess: ProgramPartnerAccess, targetPopulationsCount?: number | null, dataCollectingType?: { __typename?: 'DataCollectingTypeNode', id: string, code: string, label: string, active: boolean, individualFiltersAvailable: boolean, householdFiltersAvailable: boolean, description: string, type?: DataCollectingTypeType | null } | null, partners?: Array<{ __typename?: 'PartnerNode', id: string, name?: string | null, areaAccess?: string | null, areas?: Array<{ __typename?: 'AreaNode', id: string, level: number } | null> | null } | null> | null, registrationImports: { __typename?: 'RegistrationDataImportNodeConnection', totalCount?: number | null }, pduFields?: Array<{ __typename?: 'PeriodicFieldNode', id: string, label: any, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null> | null } | null }; export type ProgrammeChoiceDataQueryVariables = Exact<{ [key: string]: never; }>; @@ -11375,7 +11398,7 @@ export type AllActiveTargetPopulationsQuery = { __typename?: 'Query', allActiveT export type AllFieldsAttributesQueryVariables = Exact<{ [key: string]: never; }>; -export type AllFieldsAttributesQuery = { __typename?: 'Query', allFieldsAttributes?: Array<{ __typename?: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, associatedWith?: string | null, isFlexField?: boolean | null } | null> | null }; +export type AllFieldsAttributesQuery = { __typename?: 'Query', allFieldsAttributes?: Array<{ __typename?: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, associatedWith?: string | null, isFlexField?: boolean | null, type?: string | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null> | null }; export type AllSteficonRulesQueryVariables = Exact<{ enabled?: InputMaybe; @@ -11428,7 +11451,7 @@ export type TargetPopulationQueryVariables = Exact<{ }>; -export type TargetPopulationQuery = { __typename?: 'Query', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, isFlexField: boolean, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null } | null } | null> | null } | null> | null } | null } | null }; +export type TargetPopulationQuery = { __typename?: 'Query', targetPopulation?: { __typename?: 'TargetPopulationNode', id: string, name: string, status: TargetPopulationStatus, adminUrl?: string | null, buildStatus: TargetPopulationBuildStatus, totalHouseholdsCount?: number | null, totalIndividualsCount?: number | null, childMaleCount?: number | null, childFemaleCount?: number | null, adultMaleCount?: number | null, adultFemaleCount?: number | null, caHashId?: string | null, excludedIds: string, exclusionReason: string, vulnerabilityScoreMin?: number | null, vulnerabilityScoreMax?: number | null, changeDate?: any | null, finalizedAt?: any | null, hasEmptyCriteria?: boolean | null, hasEmptyIdsCriteria?: boolean | null, steficonRule?: { __typename: 'RuleCommitNode', id: string, rule?: { __typename: 'SteficonRuleNode', id: string, name: string } | null } | null, finalizedBy?: { __typename: 'UserNode', id: string, firstName: string, lastName: string } | null, program?: { __typename: 'ProgramNode', id: string, name: string, status: ProgramStatus, startDate: any, endDate: any, isSocialWorkerProgram?: boolean | null } | null, programCycle?: { __typename: 'ProgramCycleNode', id: string, title?: string | null } | null, createdBy?: { __typename: 'UserNode', id: string, email: string, firstName: string, lastName: string } | null, targetingCriteria?: { __typename: 'TargetingCriteriaNode', id: any, flagExcludeIfActiveAdjudicationTicket: boolean, flagExcludeIfOnSanctionList: boolean, householdIds: string, individualIds: string, rules?: Array<{ __typename: 'TargetingCriteriaRuleNode', id: any, individualsFiltersBlocks?: Array<{ __typename: 'TargetingIndividualRuleFilterBlockNode', individualBlockFilters?: Array<{ __typename: 'TargetingIndividualBlockRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification, roundNumber?: number | null, arguments?: Array | null, comparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null, filters?: Array<{ __typename: 'TargetingCriteriaRuleFilterNode', id: any, fieldName: string, flexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification, arguments?: Array | null, comparisonMethod: TargetingCriteriaRuleFilterComparisonMethod, fieldAttribute?: { __typename: 'FieldAttributeNode', id?: string | null, name?: string | null, labelEn?: string | null, type?: string | null, choices?: Array<{ __typename?: 'CoreFieldChoiceObject', value?: string | null, labelEn?: string | null } | null> | null, pduData?: { __typename?: 'PeriodicFieldDataNode', id: string, subtype: PeriodicFieldDataSubtype, numberOfRounds: number, roundsNames: Array } | null } | null } | null> | null } | null> | null } | null } | null }; export type TargetPopulationHouseholdsQueryVariables = Exact<{ targetPopulation: Scalars['ID']['input']; @@ -12414,6 +12437,7 @@ export const ProgramDetailsFragmentDoc = gql` registrationImports { totalCount } + targetPopulationsCount pduFields { id label @@ -12739,7 +12763,8 @@ export const TargetPopulationDetailedFragmentDoc = gql` __typename id fieldName - isFlexField + flexFieldClassification + roundNumber arguments comparisonMethod fieldAttribute { @@ -12752,6 +12777,12 @@ export const TargetPopulationDetailedFragmentDoc = gql` value labelEn } + pduData { + id + subtype + numberOfRounds + roundsNames + } } } } @@ -12759,7 +12790,7 @@ export const TargetPopulationDetailedFragmentDoc = gql` __typename id fieldName - isFlexField + flexFieldClassification arguments comparisonMethod fieldAttribute { @@ -12772,6 +12803,12 @@ export const TargetPopulationDetailedFragmentDoc = gql` value labelEn } + pduData { + id + subtype + numberOfRounds + roundsNames + } } } } @@ -17802,6 +17839,12 @@ export const ImportedIndividualFieldsDocument = gql` admin listName } + pduData { + id + subtype + numberOfRounds + roundsNames + } } } `; @@ -23885,6 +23928,13 @@ export const AllFieldsAttributesDocument = gql` labelEn associatedWith isFlexField + type + pduData { + id + subtype + numberOfRounds + roundsNames + } } } `; @@ -24472,6 +24522,7 @@ export type ResolversTypes = { FinancialServiceProviderXlsxTemplateNodeConnection: ResolverTypeWrapper; FinancialServiceProviderXlsxTemplateNodeEdge: ResolverTypeWrapper; FinishPaymentVerificationPlan: ResolverTypeWrapper; + FlexFieldClassificationChoices: FlexFieldClassificationChoices; FlexFieldsScalar: ResolverTypeWrapper; Float: ResolverTypeWrapper; FspChoice: ResolverTypeWrapper; @@ -24743,11 +24794,13 @@ export type ResolversTypes = { TargetingCriteriaNode: ResolverTypeWrapper; TargetingCriteriaObjectType: TargetingCriteriaObjectType; TargetingCriteriaRuleFilterComparisonMethod: TargetingCriteriaRuleFilterComparisonMethod; + TargetingCriteriaRuleFilterFlexFieldClassification: TargetingCriteriaRuleFilterFlexFieldClassification; TargetingCriteriaRuleFilterNode: ResolverTypeWrapper; TargetingCriteriaRuleFilterObjectType: TargetingCriteriaRuleFilterObjectType; TargetingCriteriaRuleNode: ResolverTypeWrapper; TargetingCriteriaRuleObjectType: TargetingCriteriaRuleObjectType; TargetingIndividualBlockRuleFilterComparisonMethod: TargetingIndividualBlockRuleFilterComparisonMethod; + TargetingIndividualBlockRuleFilterFlexFieldClassification: TargetingIndividualBlockRuleFilterFlexFieldClassification; TargetingIndividualBlockRuleFilterNode: ResolverTypeWrapper; TargetingIndividualRuleFilterBlockNode: ResolverTypeWrapper; TargetingIndividualRuleFilterBlockObjectType: TargetingIndividualRuleFilterBlockObjectType; @@ -28023,6 +28076,7 @@ export type ProgramNodeResolvers; status?: Resolver; surveys?: Resolver>; + targetPopulationsCount?: Resolver, ParentType, ContextType>; targetpopulationSet?: Resolver>; totalDeliveredQuantity?: Resolver, ParentType, ContextType>; totalEntitledQuantity?: Resolver, ParentType, ContextType>; @@ -28884,8 +28938,8 @@ export type TargetingCriteriaRuleFilterNodeResolvers; fieldAttribute?: Resolver, ParentType, ContextType>; fieldName?: Resolver; + flexFieldClassification?: Resolver; id?: Resolver; - isFlexField?: Resolver; targetingCriteriaRule?: Resolver; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; @@ -28907,9 +28961,10 @@ export type TargetingIndividualBlockRuleFilterNodeResolvers; fieldAttribute?: Resolver, ParentType, ContextType>; fieldName?: Resolver; + flexFieldClassification?: Resolver; id?: Resolver; individualsFiltersBlock?: Resolver; - isFlexField?: Resolver; + roundNumber?: Resolver, ParentType, ContextType>; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/frontend/src/apollo/fragments/ProgramDetailsFragment.ts b/frontend/src/apollo/fragments/ProgramDetailsFragment.ts index 6409c85cd5..ffd8e4305c 100644 --- a/frontend/src/apollo/fragments/ProgramDetailsFragment.ts +++ b/frontend/src/apollo/fragments/ProgramDetailsFragment.ts @@ -46,6 +46,7 @@ export const programDetails = gql` registrationImports { totalCount } + targetPopulationsCount pduFields { id label diff --git a/frontend/src/apollo/fragments/TargetPopulationFragments.ts b/frontend/src/apollo/fragments/TargetPopulationFragments.ts index aaab6ffa63..5592655c41 100644 --- a/frontend/src/apollo/fragments/TargetPopulationFragments.ts +++ b/frontend/src/apollo/fragments/TargetPopulationFragments.ts @@ -99,7 +99,8 @@ export const targetPopulationDetailed = gql` id fieldName - isFlexField + flexFieldClassification + roundNumber arguments comparisonMethod fieldAttribute { @@ -112,6 +113,12 @@ export const targetPopulationDetailed = gql` value labelEn } + pduData { + id + subtype + numberOfRounds + roundsNames + } } } } @@ -119,7 +126,7 @@ export const targetPopulationDetailed = gql` __typename id fieldName - isFlexField + flexFieldClassification arguments comparisonMethod fieldAttribute { @@ -132,6 +139,12 @@ export const targetPopulationDetailed = gql` value labelEn } + pduData { + id + subtype + numberOfRounds + roundsNames + } } } } diff --git a/frontend/src/apollo/queries/core/attributes/ImportedIndividualFields.ts b/frontend/src/apollo/queries/core/attributes/ImportedIndividualFields.ts index d81a195583..03c1ebb0e7 100644 --- a/frontend/src/apollo/queries/core/attributes/ImportedIndividualFields.ts +++ b/frontend/src/apollo/queries/core/attributes/ImportedIndividualFields.ts @@ -1,8 +1,14 @@ import { gql } from '@apollo/client'; export const ImportedIndividualFields = gql` - query ImportedIndividualFields($businessAreaSlug: String, $programId: String) { - allFieldsAttributes(businessAreaSlug: $businessAreaSlug, programId: $programId) { + query ImportedIndividualFields( + $businessAreaSlug: String + $programId: String + ) { + allFieldsAttributes( + businessAreaSlug: $businessAreaSlug + programId: $programId + ) { isFlexField id type @@ -24,6 +30,12 @@ export const ImportedIndividualFields = gql` admin listName } + pduData { + id + subtype + numberOfRounds + roundsNames + } } } `; diff --git a/frontend/src/apollo/queries/targeting/AllFieldsAttributes.ts b/frontend/src/apollo/queries/targeting/AllFieldsAttributes.ts index 226ff68c9d..d4bca76329 100644 --- a/frontend/src/apollo/queries/targeting/AllFieldsAttributes.ts +++ b/frontend/src/apollo/queries/targeting/AllFieldsAttributes.ts @@ -8,6 +8,13 @@ export const AllFieldsAttributes = gql` labelEn associatedWith isFlexField + type + pduData { + id + subtype + numberOfRounds + roundsNames + } } } `; diff --git a/frontend/src/components/programs/CreateProgram/ProgramFieldSeriesStep.tsx b/frontend/src/components/programs/CreateProgram/ProgramFieldSeriesStep.tsx index ca00e467e7..a9f245e8af 100644 --- a/frontend/src/components/programs/CreateProgram/ProgramFieldSeriesStep.tsx +++ b/frontend/src/components/programs/CreateProgram/ProgramFieldSeriesStep.tsx @@ -20,6 +20,7 @@ interface ProgramFieldSeriesStepProps { setStep: (step: number) => void; step: number; programHasRdi?: boolean; + programHasTp?: boolean; pdusubtypeChoicesData?: PduSubtypeChoicesDataQuery; errors: any; programId?: string; @@ -33,6 +34,7 @@ export const ProgramFieldSeriesStep = ({ setStep, step, programHasRdi, + programHasTp, pdusubtypeChoicesData, errors, programId: formProgramId, @@ -56,6 +58,8 @@ export const ProgramFieldSeriesStep = ({ 'Are you sure you want to delete this field? This action cannot be reversed.', ); + const fieldDisabled = programHasRdi || programHasTp; + return ( <> @@ -87,7 +91,7 @@ export const ProgramFieldSeriesStep = ({ label={t('Data Type')} component={FormikSelectField} choices={mappedPduSubtypeChoices} - disabled={programHasRdi} + disabled={fieldDisabled} /> @@ -134,7 +138,7 @@ export const ProgramFieldSeriesStep = ({ choices={[...Array(20).keys()].map((n) => { const isDisabled = values.editMode && - programHasRdi && + fieldDisabled && n + 2 <= (program?.pduFields[index]?.pduData ?.numberOfRounds || 0); @@ -156,7 +160,7 @@ export const ProgramFieldSeriesStep = ({ type: 'error', }).then(() => arrayHelpers.remove(index)) } - disabled={programHasRdi} + disabled={fieldDisabled} > @@ -171,7 +175,7 @@ export const ProgramFieldSeriesStep = ({ program?.pduFields?.[index]?.pduData ?.numberOfRounds || 0; const isDisabled = - programHasRdi && + fieldDisabled && values.editMode && round + 1 <= selectedNumberOfRounds; return ( @@ -212,7 +216,7 @@ export const ProgramFieldSeriesStep = ({ }) } endIcon={} - disabled={programHasRdi} + disabled={fieldDisabled} data-cy="button-add-time-series-field" > {t('Add Time Series Fields')} diff --git a/frontend/src/components/targeting/EditTargetPopulation/EditTargetPopulation.tsx b/frontend/src/components/targeting/EditTargetPopulation/EditTargetPopulation.tsx index d861f5d8cc..c2efb5f918 100644 --- a/frontend/src/components/targeting/EditTargetPopulation/EditTargetPopulation.tsx +++ b/frontend/src/components/targeting/EditTargetPopulation/EditTargetPopulation.tsx @@ -19,8 +19,8 @@ import * as Yup from 'yup'; import { AndDivider, AndDividerLabel } from '../AndDivider'; import { Exclusions } from '../CreateTargetPopulation/Exclusions'; import { PaperContainer } from '../PaperContainer'; -import { TargetingCriteria } from '../TargetingCriteria'; import { EditTargetPopulationHeader } from './EditTargetPopulationHeader'; +import { TargetingCriteriaDisplay } from '../TargetingCriteriaDisplay/TargetingCriteriaDisplay'; import { ProgramCycleAutocompleteRest } from '@shared/autocompletes/rest/ProgramCycleAutocompleteRest'; interface EditTargetPopulationProps { @@ -193,7 +193,7 @@ export const EditTargetPopulation = ({ ( - - + {targetPopulation.totalHouseholdsCount || '0'} diff --git a/frontend/src/components/targeting/SubField.tsx b/frontend/src/components/targeting/SubField.tsx index eaca95ca9d..e33659ce85 100644 --- a/frontend/src/components/targeting/SubField.tsx +++ b/frontend/src/components/targeting/SubField.tsx @@ -1,6 +1,7 @@ import CalendarTodayRoundedIcon from '@mui/icons-material/CalendarTodayRounded'; -import { Field } from 'formik'; +import { Field, useFormikContext } from 'formik'; import * as React from 'react'; +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { FormikAutocomplete } from '@shared/Formik/FormikAutocomplete'; @@ -8,6 +9,8 @@ import { FormikDateField } from '@shared/Formik/FormikDateField'; import { FormikDecimalField } from '@shared/Formik/FormikDecimalField'; import { FormikSelectField } from '@shared/Formik/FormikSelectField'; import { FormikTextField } from '@shared/Formik/FormikTextField'; +import { Grid } from '@mui/material'; +import { FormikCheckboxField } from '@shared/Formik/FormikCheckboxField'; const FlexWrapper = styled.div` display: flex; @@ -17,163 +20,273 @@ const InlineField = styled.div` width: 48%; `; -export function SubField({ - field, - index, +interface Values { + individualsFiltersBlocks?: { + individualBlockFilters?: { + isNull?: boolean; + }[]; + }[]; +} + +interface SubFieldProps { + baseName: string; + blockIndex?: number; + index?: number; + field?: any; // Adjust the type of field as necessary + choicesDict?: any; // Adjust the type of choicesDict as necessary +} + +export const SubField: React.FC = ({ baseName, + blockIndex, + index, + field, choicesDict, -}): React.ReactElement { +}) => { const { t } = useTranslation(); - switch (field.fieldAttribute.type) { - case 'DECIMAL': - return ( - - - - - - - - - ); - case 'DATE': - return ( - - - } - data-cy="date-from" - /> - - - } - data-cy="date-to" - /> - - - ); - case 'INTEGER': - return ( - - - - - - - - - ); - case 'SELECT_ONE': - return field.fieldName.includes('admin') ? ( - - ) : ( - - ); - case 'SELECT_MANY': - return ( - - ); - case 'STRING': - return ( - - ); - case 'BOOL': - return ( - - ); - default: - return

{field.fieldAttribute.type}

; + const { values, setFieldValue } = useFormikContext(); + + if (blockIndex === undefined) { + const match = baseName.match(/block\[(\d+)\]/); + if (match) { + blockIndex = parseInt(match[1], 10); + } } -} + + const isNullSelected = + blockIndex !== undefined && index !== undefined + ? values?.individualsFiltersBlocks?.[blockIndex] + ?.individualBlockFilters?.[index]?.isNull ?? false + : false; + + useEffect(() => { + if (isNullSelected) { + setFieldValue(`${baseName}.value.from`, ''); + setFieldValue(`${baseName}.value.to`, ''); + setFieldValue(`${baseName}.value`, ''); + } + }, [isNullSelected, setFieldValue, baseName]); + + if (!field) { + return null; + } + + const renderFieldByType = (type: string) => { + switch (type) { + case 'DECIMAL': + return ( + + + + + + + + + ); + case 'DATE': + return ( + + + } + data-cy="date-from" + disabled={isNullSelected} + /> + + + } + data-cy="date-to" + disabled={isNullSelected} + /> + + + ); + case 'INTEGER': + return ( + + + + + + + + + ); + case 'SELECT_ONE': + return field.fieldName.includes('admin') ? ( + + ) : ( + + ); + case 'SELECT_MANY': + return ( + + ); + case 'STRING': + return ( + + ); + case 'BOOL': + return ( + + ); + case 'PDU': + return ( + + + ({ + value: n + 1, + name: `${n + 1}`, + })) + : [] + } + label="Round" + data-cy="input-round-number" + /> + + + + + + {renderFieldByType( + field.pduData?.subtype || + field.fieldAttribute?.pduData?.subtype, + )} + + + ); + + default: + return <>; + } + }; + + return renderFieldByType(field.fieldAttribute.type); +}; diff --git a/frontend/src/components/targeting/TargetPopulationCore.tsx b/frontend/src/components/targeting/TargetPopulationCore.tsx index 36802db922..71d8b22381 100644 --- a/frontend/src/components/targeting/TargetPopulationCore.tsx +++ b/frontend/src/components/targeting/TargetPopulationCore.tsx @@ -11,11 +11,11 @@ import { } from '@generated/graphql'; import { PaperContainer } from './PaperContainer'; import { ResultsForHouseholds } from './ResultsForHouseholds'; -import { TargetingCriteria } from './TargetingCriteria'; import { TargetingHouseholds } from './TargetingHouseholds'; import { useBaseUrl } from '@hooks/useBaseUrl'; import { TargetPopulationPeopleTable } from '@containers/tables/targeting/TargetPopulationPeopleTable'; import { ResultsForPeople } from '@components/targeting/ResultsForPeople'; +import { TargetingCriteriaDisplay } from './TargetingCriteriaDisplay/TargetingCriteriaDisplay'; const Label = styled.p` color: #b1b1b5; @@ -111,7 +111,7 @@ export const TargetPopulationCore = ({ {individualIds} )} - diff --git a/frontend/src/components/targeting/TargetPopulationFilters.tsx b/frontend/src/components/targeting/TargetPopulationTableFilters.tsx similarity index 96% rename from frontend/src/components/targeting/TargetPopulationFilters.tsx rename to frontend/src/components/targeting/TargetPopulationTableFilters.tsx index d460ec45b6..bc2b47b948 100644 --- a/frontend/src/components/targeting/TargetPopulationFilters.tsx +++ b/frontend/src/components/targeting/TargetPopulationTableFilters.tsx @@ -14,20 +14,20 @@ import { SearchTextField } from '@core/SearchTextField'; import { SelectFilter } from '@core/SelectFilter'; import { FiltersSection } from '@core/FiltersSection'; -interface TargetPopulationFiltersProps { +interface TargetPopulationTableFiltersProps { filter; setFilter: (filter) => void; initialFilter; appliedFilter; setAppliedFilter: (filter) => void; } -export const TargetPopulationFilters = ({ +export const TargetPopulationTableFilters = ({ filter, setFilter, initialFilter, appliedFilter, setAppliedFilter, -}: TargetPopulationFiltersProps): React.ReactElement => { +}: TargetPopulationTableFiltersProps): React.ReactElement => { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); diff --git a/frontend/src/components/targeting/TargetingCriteria/TargetingCriteriaDisabled.tsx b/frontend/src/components/targeting/TargetingCriteria/TargetingCriteriaDisabled.tsx deleted file mode 100644 index b3d1cc0e14..0000000000 --- a/frontend/src/components/targeting/TargetingCriteria/TargetingCriteriaDisabled.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as React from 'react'; -import styled from 'styled-components'; -import { useTranslation } from 'react-i18next'; -import { Tooltip, Box } from '@mui/material'; -import { AddCircleOutline } from '@mui/icons-material'; - - - -export const ContentWrapper = styled(Box)` - display: flex; - flex-wrap: wrap; -`; - -const IconWrapper = styled.div` - display: flex; - color: #a0b6d6; -`; - -const AddCriteria = styled.div` - display: flex; - align-items: center; - justify-content: center; - color: #003c8f; - border: 2px solid #a0b6d6; - border-radius: 3px; - font-size: 16px; - padding: ${({ theme }) => theme.spacing(6)} - ${({ theme }) => theme.spacing(28)}; - cursor: pointer; - p { - font-weight: 500; - margin: 0 0 0 ${({ theme }) => theme.spacing(2)}; - } -`; - -export function TargetingCriteriaDisabled({ - showTooltip = false, -}): React.ReactElement { - const { t } = useTranslation(); - return ( - <> - {showTooltip ? ( - - -
- null} - data-cy="button-target-population-disabled-add-criteria" - > - - -

{t('Add Filter')}

-
-
-
-
-
- ) : ( - - null} - data-cy="button-target-population-disabled-add-criteria" - > - - -

{t('Add Filter')}

-
-
-
- )} - - ); -} diff --git a/frontend/src/components/targeting/TargetingCriteria/index.ts b/frontend/src/components/targeting/TargetingCriteria/index.ts deleted file mode 100644 index 371e158105..0000000000 --- a/frontend/src/components/targeting/TargetingCriteria/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { TargetingCriteria } from './TargetingCriteria'; - -export { TargetingCriteria }; diff --git a/frontend/src/components/targeting/TargetingCriteria/Criteria.tsx b/frontend/src/components/targeting/TargetingCriteriaDisplay/Criteria.tsx similarity index 53% rename from frontend/src/components/targeting/TargetingCriteria/Criteria.tsx rename to frontend/src/components/targeting/TargetingCriteriaDisplay/Criteria.tsx index dc775e9ac7..f9270945fb 100644 --- a/frontend/src/components/targeting/TargetingCriteria/Criteria.tsx +++ b/frontend/src/components/targeting/TargetingCriteriaDisplay/Criteria.tsx @@ -6,10 +6,13 @@ import styled from 'styled-components'; import GreaterThanEqual from '../../../assets/GreaterThanEqual.svg'; import LessThanEqual from '../../../assets/LessThanEqual.svg'; import { TargetingCriteriaRuleObjectType } from '@generated/graphql'; +import { Box } from '@mui/system'; +import { BlueText } from '@components/grievances/LookUps/LookUpStyles'; interface CriteriaElementProps { alternative?: boolean; } + const CriteriaElement = styled.div` width: auto; max-width: 380px; @@ -60,6 +63,17 @@ const CriteriaSetBox = styled.div` margin: ${({ theme }) => theme.spacing(2)} 0; `; +const PduDataBox = styled(Box)` + display: flex; + justify-content: center; + align-items: center; + background-color: #fff; + border: 1px solid #ccc; + border-radius: 3px; + padding: ${({ theme }) => theme.spacing(3)}; + margin: ${({ theme }) => theme.spacing(3)}; +`; + const CriteriaField = ({ field, choicesDict }): React.ReactElement => { const extractChoiceLabel = (choiceField, argument) => { let choices = choicesDict?.[choiceField.fieldName]; @@ -70,14 +84,18 @@ const CriteriaField = ({ field, choicesDict }): React.ReactElement => { ? choices.find((each) => each.value === argument)?.labelEn : argument; }; + + const displayValueOrEmpty = (value) => (value ? value : 'Empty'); + const { t } = useTranslation(); let fieldElement; + switch (field.comparisonMethod) { case 'NOT_EQUALS': fieldElement = (

{field.fieldAttribute.labelEn || field.fieldName}:{' '} - {field.arguments[0]} + {displayValueOrEmpty(field.arguments?.[0])}

); break; @@ -86,7 +104,8 @@ const CriteriaField = ({ field, choicesDict }): React.ReactElement => {

{field.fieldAttribute.labelEn || field.fieldName}:{' '} - {field.arguments[0]} -{field.arguments[1]} + {displayValueOrEmpty(field.arguments?.[0])} -{' '} + {displayValueOrEmpty(field.arguments?.[1])}

); @@ -95,41 +114,65 @@ const CriteriaField = ({ field, choicesDict }): React.ReactElement => { fieldElement = (

{field.fieldAttribute.labelEn || field.fieldName}:{' '} - {field.fieldAttribute.type === 'BOOL' ? ( - {field.arguments[0] === 'True' ? t('Yes') : t('No')} + {field.isNull === true || field.comparisonMethod === 'IS_NULL' ? ( + {t('Empty')} + ) : typeof field.arguments?.[0] === 'boolean' ? ( + field.arguments[0] ? ( + {t('Yes')} + ) : ( + {t('No')} + ) ) : ( - {extractChoiceLabel(field, field.arguments[0])} + <> + {field.arguments?.[0] != null ? ( + typeof field.arguments[0] === 'boolean' ? ( + field.arguments[0] === true ? ( + {t('Yes')} + ) : ( + {t('No')} + ) + ) : field.arguments[0] === 'Yes' ? ( + {t('Yes')} + ) : field.arguments[0] === 'No' ? ( + {t('No')} + ) : ( + {extractChoiceLabel(field, field.arguments[0])} + ) + ) : ( + {t('Empty')} + )} + )}

); break; case 'LESS_THAN': + case 'GREATER_THAN': { + const isLessThan = field.type === 'LESS_THAN'; + const MathSignComponent = isLessThan ? LessThanEqual : GreaterThanEqual; + const altText = isLessThan ? 'less_than' : 'greater_than'; + const displayValue = field.arguments?.[0]; + fieldElement = (

{field.fieldAttribute.labelEn || field.fieldName}:{' '} - - {field.arguments[0]} -

- ); - break; - case 'GREATER_THAN': - fieldElement = ( -

- {field.fieldAttribute.labelEn || field.fieldName}:{' '} - - {field.arguments[0]} + {displayValue && } + {displayValueOrEmpty(displayValue)}

); break; + } case 'CONTAINS': fieldElement = (

{field.fieldAttribute.labelEn || field.fieldName}:{' '} - {field.arguments.map((argument, index) => ( - <> - {extractChoiceLabel(field, argument)} + {field.arguments?.map((argument, index) => ( + + + {displayValueOrEmpty(extractChoiceLabel(field, argument))} + {index !== field.arguments.length - 1 && ', '} - + ))}

); @@ -137,12 +180,38 @@ const CriteriaField = ({ field, choicesDict }): React.ReactElement => { default: fieldElement = (

- {field.fieldAttribute.labelEn}:{field.arguments[0]} + {field.fieldAttribute.labelEn}:{' '} + {displayValueOrEmpty(field.arguments?.[0])}

); break; } - return fieldElement; + + return ( + <> + {fieldElement} + {field.fieldAttribute.type === 'PDU' && + (field.pduData || field.fieldAttribute.pduData) && ( + + Round {field.roundNumber} + {(field.pduData || field.fieldAttribute.pduData).roundsNames[ + field.roundNumber - 1 + ] && ( + <> + {' '} + ( + { + (field.pduData || field.fieldAttribute.pduData).roundsNames[ + field.roundNumber - 1 + ] + } + ) + + )} + + )} + + ); }; interface CriteriaProps { @@ -168,16 +237,17 @@ export function Criteria({ }: CriteriaProps): React.ReactElement { return ( - {rules.map((each, index) => ( - // eslint-disable-next-line + {(rules || []).map((each, index) => ( ))} - {individualsFiltersBlocks.map((item) => ( - // eslint-disable-next-line - - {item.individualBlockFilters.map((filter) => ( - // eslint-disable-next-line - + {individualsFiltersBlocks.map((item, index) => ( + + {item.individualBlockFilters.map((filter, filterIndex) => ( + ))} ))} @@ -187,7 +257,7 @@ export function Criteria({ {canRemove && ( - + )} diff --git a/frontend/src/components/targeting/TargetingCriteria/CriteriaAutocomplete.tsx b/frontend/src/components/targeting/TargetingCriteriaDisplay/CriteriaAutocomplete.tsx similarity index 98% rename from frontend/src/components/targeting/TargetingCriteria/CriteriaAutocomplete.tsx rename to frontend/src/components/targeting/TargetingCriteriaDisplay/CriteriaAutocomplete.tsx index dc8422ce6d..d8ab7a3654 100644 --- a/frontend/src/components/targeting/TargetingCriteria/CriteriaAutocomplete.tsx +++ b/frontend/src/components/targeting/TargetingCriteriaDisplay/CriteriaAutocomplete.tsx @@ -31,7 +31,7 @@ export function CriteriaAutocomplete({ index === self.findIndex((t) => t.name === choice.name), ); setChoicesWithoutDuplicates(uniqueChoices); - }, [ otherProps.choices]); + }, [otherProps.choices]); const isInvalid = get(otherProps.form.errors, field.name) && get(otherProps.form.touched, field.name); diff --git a/frontend/src/components/targeting/TargetingCriteria/TargetingCriteria.tsx b/frontend/src/components/targeting/TargetingCriteriaDisplay/TargetingCriteriaDisplay.tsx similarity index 96% rename from frontend/src/components/targeting/TargetingCriteria/TargetingCriteria.tsx rename to frontend/src/components/targeting/TargetingCriteriaDisplay/TargetingCriteriaDisplay.tsx index 585d27c090..5b6def0d60 100644 --- a/frontend/src/components/targeting/TargetingCriteria/TargetingCriteria.tsx +++ b/frontend/src/components/targeting/TargetingCriteriaDisplay/TargetingCriteriaDisplay.tsx @@ -1,4 +1,4 @@ -import { TargetCriteriaForm } from '@containers/forms/TargetCriteriaForm'; +import { TargetingCriteriaForm } from '@containers/forms/TargetingCriteriaForm'; import { DataCollectingTypeType, TargetPopulationQuery, @@ -15,8 +15,8 @@ import styled from 'styled-components'; import { Criteria } from './Criteria'; import { ContentWrapper, - TargetingCriteriaDisabled, -} from './TargetingCriteriaDisabled'; + TargetingCriteriaDisplayDisabled, +} from './TargetingCriteriaDisplayDisabled'; import { VulnerabilityScoreComponent } from './VulnerabilityScoreComponent'; import { useProgramContext } from 'src/programContext'; import { useCachedImportedIndividualFieldsQuery } from '@hooks/useCachedImportedIndividualFields'; @@ -77,7 +77,7 @@ const NoWrapCheckbox = styled(FormControlLabel)` white-space: nowrap; `; -interface TargetingCriteriaProps { +interface TargetingCriteriaDisplayProps { rules?; helpers?; targetPopulation?: TargetPopulationQuery['targetPopulation']; @@ -88,7 +88,7 @@ interface TargetingCriteriaProps { category: string; } -export const TargetingCriteria = ({ +export const TargetingCriteriaDisplay = ({ rules, helpers, targetPopulation, @@ -97,17 +97,19 @@ export const TargetingCriteria = ({ isSocialDctType, isStandardDctType, category, -}: TargetingCriteriaProps): React.ReactElement => { +}: TargetingCriteriaDisplayProps): React.ReactElement => { const { t } = useTranslation(); const location = useLocation(); const { selectedProgram } = useProgramContext(); const { businessArea, programId } = useBaseUrl(); + const { data: allCoreFieldsAttributesData, loading } = useCachedImportedIndividualFieldsQuery(businessArea, programId); const [isOpen, setOpen] = useState(false); const [criteriaIndex, setIndex] = useState(null); const [criteriaObject, setCriteria] = useState({}); const [allDataChoicesDict, setAllDataChoicesDict] = useState(null); + useEffect(() => { if (loading) return; const allDataChoicesDictTmp = @@ -117,6 +119,7 @@ export const TargetingCriteria = ({ }, {}); setAllDataChoicesDict(allDataChoicesDictTmp); }, [allCoreFieldsAttributesData, loading]); + const regex = /(create|edit-tp)/; const isDetailsPage = !regex.test(location.pathname); const openModal = (criteria): void => { @@ -179,14 +182,13 @@ export const TargetingCriteria = ({ onClick={() => setOpen(true)} data-cy="button-target-population-add-criteria" > - {t('Add')} 'Or' - {t('Filter')} + {t('Add')} 'Or' {t('Filter')} )} )} - closeModal()} @@ -199,7 +201,7 @@ export const TargetingCriteria = ({ {rules.length - ? rules.map((criteria, index) => ( + ? rules?.map((criteria, index) => ( // eslint-disable-next-line ); } - return ; + return ; }; diff --git a/frontend/src/components/targeting/TargetingCriteriaDisplay/TargetingCriteriaDisplayDisabled.tsx b/frontend/src/components/targeting/TargetingCriteriaDisplay/TargetingCriteriaDisplayDisabled.tsx new file mode 100644 index 0000000000..86dd3c5997 --- /dev/null +++ b/frontend/src/components/targeting/TargetingCriteriaDisplay/TargetingCriteriaDisplayDisabled.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; +import { Tooltip, Box } from '@mui/material'; +import { AddCircleOutline } from '@mui/icons-material'; + +export const ContentWrapper = styled(Box)` + display: flex; + flex-wrap: wrap; +`; + +const IconWrapper = styled.div` + display: flex; + color: #a0b6d6; +`; + +const AddCriteria = styled.div` + display: flex; + align-items: center; + justify-content: center; + color: #003c8f; + border: 2px solid #a0b6d6; + border-radius: 3px; + font-size: 16px; + padding: ${({ theme }) => theme.spacing(6)} + ${({ theme }) => theme.spacing(28)}; + cursor: pointer; + p { + font-weight: 500; + margin: 0 0 0 ${({ theme }) => theme.spacing(2)}; + } +`; + +export function TargetingCriteriaDisplayDisabled({ + showTooltip = false, +}): React.ReactElement { + const { t } = useTranslation(); + return ( + <> + {showTooltip ? ( + + +
+ null} + data-cy="button-target-population-disabled-add-criteria" + > + + +

{t('Add Filter')}

+
+
+
+
+
+ ) : ( + + null} + data-cy="button-target-population-disabled-add-criteria" + > + + +

{t('Add Filter')}

+
+
+
+ )} + + ); +} diff --git a/frontend/src/components/targeting/TargetingCriteria/VulnerabilityScoreComponent.tsx b/frontend/src/components/targeting/TargetingCriteriaDisplay/VulnerabilityScoreComponent.tsx similarity index 100% rename from frontend/src/components/targeting/TargetingCriteria/VulnerabilityScoreComponent.tsx rename to frontend/src/components/targeting/TargetingCriteriaDisplay/VulnerabilityScoreComponent.tsx diff --git a/frontend/src/containers/forms/TargetCriteriaForm.tsx b/frontend/src/containers/forms/TargetCriteriaForm.tsx deleted file mode 100644 index 20ffc3d0e4..0000000000 --- a/frontend/src/containers/forms/TargetCriteriaForm.tsx +++ /dev/null @@ -1,387 +0,0 @@ -import { - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Typography, -} from '@mui/material'; -import * as React from 'react'; -import { AddCircleOutline } from '@mui/icons-material'; -import { FieldArray, Formik } from 'formik'; -import { useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import * as Yup from 'yup'; -import { AutoSubmitFormOnEnter } from '@components/core/AutoSubmitFormOnEnter'; -import { useBaseUrl } from '@hooks/useBaseUrl'; -import { useCachedImportedIndividualFieldsQuery } from '@hooks/useCachedImportedIndividualFields'; -import { - chooseFieldType, - clearField, - formatCriteriaFilters, - formatCriteriaIndividualsFiltersBlocks, - mapCriteriaToInitialValues, -} from '@utils/targetingUtils'; -import { DialogContainer } from '../dialogs/DialogContainer'; -import { DialogDescription } from '../dialogs/DialogDescription'; -import { DialogFooter } from '../dialogs/DialogFooter'; -import { DialogTitleWrapper } from '../dialogs/DialogTitleWrapper'; -import { TargetingCriteriaFilter } from './TargetCriteriaFilter'; -import { TargetCriteriaFilterBlocks } from './TargetCriteriaFilterBlocks'; -import { AndDivider, AndDividerLabel } from '@components/targeting/AndDivider'; - -const ButtonBox = styled.div` - width: 300px; -`; -const DialogError = styled.div` - margin: 20px 0; - font-size: 14px; - color: ${({ theme }) => theme.palette.error.dark}; -`; - -const StyledBox = styled(Box)` - width: 100%; -`; - -const validationSchema = Yup.object().shape({ - filters: Yup.array().of( - Yup.object().shape({ - fieldName: Yup.string().required('Field Type is required'), - }), - ), - individualsFiltersBlocks: Yup.array().of( - Yup.object().shape({ - individualBlockFilters: Yup.array().of( - Yup.object().shape({ - fieldName: Yup.string().required('Field Type is required'), - }), - ), - }), - ), -}); -interface ArrayFieldWrapperProps { - arrayHelpers; - children: React.ReactNode; -} -class ArrayFieldWrapper extends React.Component { - getArrayHelpers(): object { - const { arrayHelpers } = this.props; - return arrayHelpers; - } - - render(): React.ReactNode { - const { children } = this.props; - return children; - } -} - -interface TargetCriteriaFormPropTypes { - criteria?; - addCriteria: (values) => void; - open: boolean; - onClose: () => void; - individualFiltersAvailable: boolean; - householdFiltersAvailable: boolean; - isSocialWorkingProgram: boolean; -} - -const associatedWith = (type) => (item) => item.associatedWith === type; -const isNot = (type) => (item) => item.type !== type; - -export const TargetCriteriaForm = ({ - criteria, - addCriteria, - open, - onClose, - individualFiltersAvailable, - householdFiltersAvailable, - isSocialWorkingProgram, -}: TargetCriteriaFormPropTypes): React.ReactElement => { - const { t } = useTranslation(); - const { businessArea, programId } = useBaseUrl(); - - const { data, loading } = useCachedImportedIndividualFieldsQuery( - businessArea, - programId, - ); - - const filtersArrayWrapperRef = useRef(null); - const individualsFiltersBlocksWrapperRef = useRef(null); - const initialValue = mapCriteriaToInitialValues(criteria); - const [individualData, setIndividualData] = useState(null); - const [householdData, setHouseholdData] = useState(null); - const [allDataChoicesDict, setAllDataChoicesDict] = useState(null); - useEffect(() => { - if (loading) return; - const filteredIndividualData = { - allFieldsAttributes: data?.allFieldsAttributes - ?.filter(associatedWith('Individual')) - .filter(isNot('IMAGE')), - }; - setIndividualData(filteredIndividualData); - - const filteredHouseholdData = { - allFieldsAttributes: data?.allFieldsAttributes?.filter( - associatedWith('Household'), - ), - }; - setHouseholdData(filteredHouseholdData); - const allDataChoicesDictTmp = data?.allFieldsAttributes?.reduce( - (acc, item) => { - acc[item.name] = item.choices; - return acc; - }, - {}, - ); - setAllDataChoicesDict(allDataChoicesDictTmp); - }, [data, loading]); - - if (!data) return null; - - const validate = ({ - filters, - individualsFiltersBlocks, - }): { nonFieldErrors?: string[] } => { - const filterNullOrNoSelections = (filter): boolean => - filter.value === null || - filter.value === '' || - (filter?.fieldAttribute?.type === 'SELECT_MANY' && - filter.value && - filter.value.length === 0); - - const filterEmptyFromTo = (filter): boolean => - typeof filter.value === 'object' && - filter.value !== null && - Object.prototype.hasOwnProperty.call(filter.value, 'from') && - Object.prototype.hasOwnProperty.call(filter.value, 'to') && - !filter.value.from && - !filter.value.to; - - const hasFiltersNullValues = Boolean( - filters.filter(filterNullOrNoSelections).length, - ); - - const hasFiltersEmptyFromToValues = Boolean( - filters.filter(filterEmptyFromTo).length, - ); - - const hasFiltersErrors = - hasFiltersNullValues || hasFiltersEmptyFromToValues; - - const hasIndividualsFiltersBlocksErrors = individualsFiltersBlocks.some( - (block) => { - const hasNulls = block.individualBlockFilters.some( - filterNullOrNoSelections, - ); - const hasFromToError = - block.individualBlockFilters.some(filterEmptyFromTo); - - return hasNulls || hasFromToError; - }, - ); - - const errors: { nonFieldErrors?: string[] } = {}; - if (hasFiltersErrors || hasIndividualsFiltersBlocksErrors) { - errors.nonFieldErrors = ['You need to fill out missing values.']; - } - if (filters.length + individualsFiltersBlocks.length === 0) { - errors.nonFieldErrors = [ - 'You need to add at least one household filter or an individual block filter.', - ]; - } else if ( - individualsFiltersBlocks.filter( - (block) => block.individualBlockFilters.length === 0, - ).length > 0 - ) { - errors.nonFieldErrors = [ - 'You need to add at least one household filter or an individual block filter.', - ]; - } - return errors; - }; - - const handleSubmit = (values, bag): void => { - const filters = formatCriteriaFilters(values.filters); - const individualsFiltersBlocks = formatCriteriaIndividualsFiltersBlocks( - values.individualsFiltersBlocks, - ); - addCriteria({ filters, individualsFiltersBlocks }); - return bag.resetForm(); - }; - if (loading || !open) return null; - - return ( - - - {({ submitForm, values, resetForm, errors }) => ( - - {open && } - - - - {t('Add Filter')} - - - - - { - // @ts-ignore - errors.nonFieldErrors && ( - -
    - { - // @ts-ignore - errors.nonFieldErrors.map((message) => ( -
  • {message}
  • - )) - } -
-
- ) - } - - {isSocialWorkingProgram - ? '' - : 'All rules defined below have to be true for the entire household.'} - - ( - - {values.filters.map((each, index) => ( - { - if (object) { - return chooseFieldType(object, arrayHelpers, index); - } - return clearField(arrayHelpers, index); - }} - values={values} - onClick={() => arrayHelpers.remove(index)} - /> - ))} - - )} - /> - {householdFiltersAvailable || isSocialWorkingProgram ? ( - - - - - - ) : null} - {individualFiltersAvailable && !isSocialWorkingProgram ? ( - <> - {householdFiltersAvailable ? ( - - And - - ) : null} - ( - - {values.individualsFiltersBlocks.map((each, index) => ( - arrayHelpers.remove(index)} - /> - ))} - - )} - /> - - - - - - - ) : null} -
- - - -
- - -
-
-
-
-
- )} -
-
- ); -}; diff --git a/frontend/src/containers/forms/TargetingCriteriaForm.tsx b/frontend/src/containers/forms/TargetingCriteriaForm.tsx new file mode 100644 index 0000000000..1d42b30bfa --- /dev/null +++ b/frontend/src/containers/forms/TargetingCriteriaForm.tsx @@ -0,0 +1,419 @@ +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Typography, +} from '@mui/material'; +import * as React from 'react'; +import { AddCircleOutline } from '@mui/icons-material'; +import { FieldArray, Formik } from 'formik'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import * as Yup from 'yup'; +import { AutoSubmitFormOnEnter } from '@components/core/AutoSubmitFormOnEnter'; +import { useBaseUrl } from '@hooks/useBaseUrl'; +import { useCachedImportedIndividualFieldsQuery } from '@hooks/useCachedImportedIndividualFields'; +import { + chooseFieldType, + clearField, + formatCriteriaFilters, + formatCriteriaIndividualsFiltersBlocks, + mapCriteriaToInitialValues, +} from '@utils/targetingUtils'; +import { DialogContainer } from '../dialogs/DialogContainer'; +import { DialogDescription } from '../dialogs/DialogDescription'; +import { DialogFooter } from '../dialogs/DialogFooter'; +import { DialogTitleWrapper } from '../dialogs/DialogTitleWrapper'; +import { TargetingCriteriaIndividualFilterBlocks } from './TargetingCriteriaIndividualFilterBlocks'; +import { AndDivider, AndDividerLabel } from '@components/targeting/AndDivider'; +import { TargetingCriteriaHouseholdFilter } from './TargetingCriteriaHouseholdFilter'; + +const ButtonBox = styled.div` + width: 300px; +`; +const DialogError = styled.div` + margin: 20px 0; + font-size: 14px; + color: ${({ theme }) => theme.palette.error.dark}; +`; + +const StyledBox = styled(Box)` + width: 100%; +`; + +const validationSchema = Yup.object().shape({ + filters: Yup.array().of( + Yup.object().shape({ + fieldName: Yup.string().required('Field Type is required'), + }), + ), + individualsFiltersBlocks: Yup.array().of( + Yup.object().shape({ + individualBlockFilters: Yup.array().of( + Yup.object().shape({ + fieldName: Yup.string().required('Field Type is required'), + fieldAttribute: Yup.object().shape({ + type: Yup.string().required(), + }), + roundNumber: Yup.string() + .nullable() + .when( + ['fieldName', 'fieldAttribute'], + (fieldName, fieldAttribute, schema) => { + const parent = schema.parent; + if ( + parent && + parent.fieldAttribute && + parent.fieldAttribute.type === 'PDU' + ) { + return Yup.string().required('Round Number is required'); + } + return Yup.string().notRequired(); + }, + ), + }), + ), + }), + ), +}); + +interface ArrayFieldWrapperProps { + arrayHelpers; + children: React.ReactNode; +} +class ArrayFieldWrapper extends React.Component { + getArrayHelpers(): object { + const { arrayHelpers } = this.props; + return arrayHelpers; + } + + render(): React.ReactNode { + const { children } = this.props; + return children; + } +} + +interface TargetingCriteriaFormPropTypes { + criteria?; + addCriteria: (values) => void; + open: boolean; + onClose: () => void; + individualFiltersAvailable: boolean; + householdFiltersAvailable: boolean; + isSocialWorkingProgram: boolean; +} + +const associatedWith = (type) => (item) => item.associatedWith === type; +const isNot = (type) => (item) => item.type !== type; + +export const TargetingCriteriaForm = ({ + criteria, + addCriteria, + open, + onClose, + individualFiltersAvailable, + householdFiltersAvailable, + isSocialWorkingProgram, +}: TargetingCriteriaFormPropTypes): React.ReactElement => { + const { t } = useTranslation(); + const { businessArea, programId } = useBaseUrl(); + + const { data, loading } = useCachedImportedIndividualFieldsQuery( + businessArea, + programId, + ); + + const filtersArrayWrapperRef = useRef(null); + const individualsFiltersBlocksWrapperRef = useRef(null); + const initialValue = mapCriteriaToInitialValues(criteria); + const [individualData, setIndividualData] = useState(null); + const [householdData, setHouseholdData] = useState(null); + const [allDataChoicesDict, setAllDataChoicesDict] = useState(null); + useEffect(() => { + if (loading) return; + const filteredIndividualData = { + allFieldsAttributes: data?.allFieldsAttributes + ?.filter(associatedWith('Individual')) + .filter(isNot('IMAGE')), + }; + setIndividualData(filteredIndividualData); + + const filteredHouseholdData = { + allFieldsAttributes: data?.allFieldsAttributes?.filter( + associatedWith('Household'), + ), + }; + setHouseholdData(filteredHouseholdData); + const allDataChoicesDictTmp = data?.allFieldsAttributes?.reduce( + (acc, item) => { + acc[item.name] = item.choices; + return acc; + }, + {}, + ); + setAllDataChoicesDict(allDataChoicesDictTmp); + }, [data, loading]); + + if (!data) return null; + + const validate = ({ + filters, + individualsFiltersBlocks, + }): { nonFieldErrors?: string[] } => { + const filterNullOrNoSelections = (filter): boolean => + !filter.isNull && + (filter.value === null || + filter.value === '' || + (filter?.fieldAttribute?.type === 'SELECT_MANY' && + filter.value && + filter.value.length === 0)); + + const filterEmptyFromTo = (filter): boolean => + !filter.isNull && + typeof filter.value === 'object' && + filter.value !== null && + Object.prototype.hasOwnProperty.call(filter.value, 'from') && + Object.prototype.hasOwnProperty.call(filter.value, 'to') && + !filter.value.from && + !filter.value.to; + + const hasFiltersNullValues = Boolean( + filters.filter(filterNullOrNoSelections).length, + ); + + const hasFiltersEmptyFromToValues = Boolean( + filters.filter(filterEmptyFromTo).length, + ); + + const hasFiltersErrors = + hasFiltersNullValues || hasFiltersEmptyFromToValues; + + const hasIndividualsFiltersBlocksErrors = individualsFiltersBlocks.some( + (block) => { + const hasNulls = block.individualBlockFilters.some( + filterNullOrNoSelections, + ); + const hasFromToError = + block.individualBlockFilters.some(filterEmptyFromTo); + + return hasNulls || hasFromToError; + }, + ); + + const errors: { nonFieldErrors?: string[] } = {}; + if (hasFiltersErrors || hasIndividualsFiltersBlocksErrors) { + errors.nonFieldErrors = ['You need to fill out missing values.']; + } + if (filters.length + individualsFiltersBlocks.length === 0) { + errors.nonFieldErrors = [ + 'You need to add at least one household filter or an individual block filter.', + ]; + } else if ( + individualsFiltersBlocks.filter( + (block) => block.individualBlockFilters.length === 0, + ).length > 0 + ) { + errors.nonFieldErrors = [ + 'You need to add at least one household filter or an individual block filter.', + ]; + } + return errors; + }; + + const handleSubmit = (values, bag): void => { + const filters = formatCriteriaFilters(values.filters); + + const individualsFiltersBlocks = formatCriteriaIndividualsFiltersBlocks( + values.individualsFiltersBlocks, + ); + addCriteria({ filters, individualsFiltersBlocks }); + return bag.resetForm(); + }; + if (loading || !open) return null; + + return ( + + + {({ submitForm, values, resetForm, errors }) => { + return ( + + {open && } + + + + {t('Add Filter')} + + + + + { + // @ts-ignore + errors.nonFieldErrors && ( + +
    + { + // @ts-ignore + errors.nonFieldErrors.map((message) => ( +
  • {message}
  • + )) + } +
+
+ ) + } + + {isSocialWorkingProgram + ? '' + : 'All rules defined below have to be true for the entire household.'} + + ( + + {values.filters.map((each, index) => ( + { + if (object) { + return chooseFieldType( + object, + arrayHelpers, + index, + ); + } + return clearField(arrayHelpers, index); + }} + values={values} + onClick={() => arrayHelpers.remove(index)} + /> + ))} + + )} + /> + {householdFiltersAvailable || isSocialWorkingProgram ? ( + + + + + + ) : null} + {individualFiltersAvailable && !isSocialWorkingProgram ? ( + <> + {householdFiltersAvailable ? ( + + And + + ) : null} + ( + + {values.individualsFiltersBlocks.map( + (each, index) => ( + arrayHelpers.remove(index)} + /> + ), + )} + + )} + /> + + + + + + + ) : null} +
+ + + +
+ + +
+
+
+
+
+ ); + }} +
+
+ ); +}; diff --git a/frontend/src/containers/forms/TargetCriteriaFilter.tsx b/frontend/src/containers/forms/TargetingCriteriaHouseholdFilter.tsx similarity index 97% rename from frontend/src/containers/forms/TargetCriteriaFilter.tsx rename to frontend/src/containers/forms/TargetingCriteriaHouseholdFilter.tsx index c7da1004c9..87133b3e77 100644 --- a/frontend/src/containers/forms/TargetCriteriaFilter.tsx +++ b/frontend/src/containers/forms/TargetingCriteriaHouseholdFilter.tsx @@ -30,7 +30,7 @@ const DividerLabel = styled.div` background-color: #fff; `; -export function TargetingCriteriaFilter({ +export function TargetingCriteriaHouseholdFilter({ index, data, each, diff --git a/frontend/src/containers/forms/TargetCriteriaBlockFilter.tsx b/frontend/src/containers/forms/TargetingCriteriaIndividualBlockFilter.tsx similarity index 92% rename from frontend/src/containers/forms/TargetCriteriaBlockFilter.tsx rename to frontend/src/containers/forms/TargetingCriteriaIndividualBlockFilter.tsx index 0c9af6e230..b902c520dc 100644 --- a/frontend/src/containers/forms/TargetCriteriaBlockFilter.tsx +++ b/frontend/src/containers/forms/TargetingCriteriaIndividualBlockFilter.tsx @@ -3,7 +3,7 @@ import { SubField } from '@components/targeting/SubField'; import { ImportedIndividualFieldsQuery } from '@generated/graphql'; import { FieldChooser } from '@components/targeting/FieldChooser'; -export function TargetCriteriaBlockFilter({ +export function TargetingCriteriaIndividualBlockFilter({ blockIndex, index, data, @@ -35,6 +35,7 @@ export function TargetCriteriaBlockFilter({
theme.spacing(3)} ${({ theme }) => theme.spacing(5)}; `; -export function TargetCriteriaFilterBlocks({ +export function TargetingCriteriaIndividualFilterBlocks({ blockIndex, data, values, @@ -101,7 +102,7 @@ export function TargetCriteriaFilterBlocks({ return ( - { partnerAccess = ProgramPartnerAccess.AllPartnersAccess, registrationImports, pduFields, + targetPopulationsCount, } = data.program; - const programHasRdi = registrationImports.totalCount > 0; + const programHasRdi = registrationImports?.totalCount > 0; + const programHasTp = targetPopulationsCount > 0; const handleSubmit = async (values): Promise => { const budgetValue = parseFloat(values.budget) ?? 0; @@ -350,6 +352,7 @@ export const EditProgramPage = (): ReactElement => { setErrors={setErrors} setFieldTouched={setFieldTouched} programHasRdi={programHasRdi} + programHasTp={programHasTp} programId={id} program={data.program} setFieldValue={setFieldValue} diff --git a/frontend/src/containers/pages/targeting/CreateTargetPopulationPage.tsx b/frontend/src/containers/pages/targeting/CreateTargetPopulationPage.tsx index 08a3b5fda4..0b5063fff6 100644 --- a/frontend/src/containers/pages/targeting/CreateTargetPopulationPage.tsx +++ b/frontend/src/containers/pages/targeting/CreateTargetPopulationPage.tsx @@ -13,7 +13,6 @@ import { PermissionDenied } from '@components/core/PermissionDenied'; import { CreateTargetPopulationHeader } from '@components/targeting/CreateTargetPopulation/CreateTargetPopulationHeader'; import { Exclusions } from '@components/targeting/CreateTargetPopulation/Exclusions'; import { PaperContainer } from '@components/targeting/PaperContainer'; -import { TargetingCriteria } from '@components/targeting/TargetingCriteria'; import { PERMISSIONS, hasPermissions } from '../../../config/permissions'; import { useBaseUrl } from '@hooks/useBaseUrl'; import { usePermissions } from '@hooks/usePermissions'; @@ -23,6 +22,7 @@ import { FormikTextField } from '@shared/Formik/FormikTextField'; import { useProgramContext } from 'src/programContext'; import { AndDivider, AndDividerLabel } from '@components/targeting/AndDivider'; import { FormikCheckboxField } from '@shared/Formik/FormikCheckboxField'; +import { TargetingCriteriaDisplay } from '@components/targeting/TargetingCriteriaDisplay/TargetingCriteriaDisplay'; import { ProgramCycleAutocompleteRest } from '@shared/autocompletes/rest/ProgramCycleAutocompleteRest'; export const CreateTargetPopulationPage = (): React.ReactElement => { @@ -175,7 +175,7 @@ export const CreateTargetPopulationPage = (): React.ReactElement => { ( - { const location = useLocation(); const { t } = useTranslation(); const permissions = usePermissions(); - const { programId } = useBaseUrl(); + const { programId } = useBaseUrl(); const { data: programData } = useProgramQuery({ variables: { id: programId }, }); @@ -51,7 +51,7 @@ export const TargetPopulationsPage = (): React.ReactElement => { return ; if (!programData) return null; let Table = TargetPopulationTable; - let Filters = TargetPopulationFilters; + let Filters = TargetPopulationTableFilters; if (programData.program.isSocialWorkerProgram) { Table = TargetPopulationForPeopleTable; Filters = TargetPopulationForPeopleFilters; diff --git a/frontend/src/shared/Formik/FormikDateField/FormikDateField.tsx b/frontend/src/shared/Formik/FormikDateField/FormikDateField.tsx index 85a1e52cc7..b961f912a3 100644 --- a/frontend/src/shared/Formik/FormikDateField/FormikDateField.tsx +++ b/frontend/src/shared/Formik/FormikDateField/FormikDateField.tsx @@ -44,6 +44,9 @@ export const FormikDateField = ({ size: 'small', error: isInvalid, helperText: isInvalid && get(form.errors, field.name), + inputProps: { + 'data-cy': `date-input-${field.name}`, + }, }, }} sx={{ @@ -71,9 +74,8 @@ export const FormikDateField = ({ ), }} required={required} - // https://github.com/mui-org/material-ui/issues/12805 - // eslint-disable-next-line react/jsx-no-duplicate-props inputProps={{ + ...props.inputProps, 'data-cy': `date-input-${field.name}`, }} /> diff --git a/frontend/src/utils/en.json b/frontend/src/utils/en.json index 68ca54eb60..812ddc1578 100644 --- a/frontend/src/utils/en.json +++ b/frontend/src/utils/en.json @@ -893,6 +893,8 @@ "Mark as Distinct": "Mark as Distinct", "Uniqueness": "Uniqueness", "Are you sure you want to clear the selected ids? They won't be marked as a duplicate or distinct anymore.": "Are you sure you want to clear the selected ids? They won't be marked as a duplicate or distinct anymore.", + "Include records with null value for the round": "Include records with null value for the round", "Programme does not have defined fields for periodic updates.": "Programme does not have defined fields for periodic updates.", - "There are no records available": "There are no records available" + "There are no records available": "There are no records available", + "Only Empty Values": "Only Empty Values" } diff --git a/frontend/src/utils/targetingUtils.ts b/frontend/src/utils/targetingUtils.ts index dc40476439..acb79e6c31 100644 --- a/frontend/src/utils/targetingUtils.ts +++ b/frontend/src/utils/targetingUtils.ts @@ -1,38 +1,51 @@ -export const chooseFieldType = (value, arrayHelpers, index): void => { - const values = { - isFlexField: value.isFlexField, - associatedWith: value.associatedWith, +export const chooseFieldType = (fieldValue, arrayHelpers, index): void => { + let flexFieldClassification; + if (fieldValue.isFlexField === false) { + flexFieldClassification = 'NOT_FLEX_FIELD'; + } else if (fieldValue.isFlexField === true && fieldValue.type !== 'PDU') { + flexFieldClassification = 'FLEX_FIELD_BASIC'; + } else if (fieldValue.isFlexField === true && fieldValue.type === 'PDU') { + flexFieldClassification = 'FLEX_FIELD_PDU'; + } + + const updatedFieldValues = { + flexFieldClassification, + associatedWith: fieldValue.associatedWith, fieldAttribute: { - labelEn: value.labelEn, - type: value.type, + labelEn: fieldValue.labelEn, + type: fieldValue.type, choices: null, }, value: null, + pduData: fieldValue.pduData, }; - switch (value.type) { + + switch (fieldValue.type) { case 'INTEGER': - values.value = { from: '', to: '' }; + updatedFieldValues.value = { from: '', to: '' }; break; case 'DATE': - values.value = { from: undefined, to: undefined }; + updatedFieldValues.value = { from: undefined, to: undefined }; break; case 'SELECT_ONE': - values.fieldAttribute.choices = value.choices; + updatedFieldValues.fieldAttribute.choices = fieldValue.choices; break; case 'SELECT_MANY': - values.value = []; - values.fieldAttribute.choices = value.choices; + updatedFieldValues.value = []; + updatedFieldValues.fieldAttribute.choices = fieldValue.choices; break; default: - values.value = null; + updatedFieldValues.value = null; break; } + arrayHelpers.replace(index, { - ...values, - fieldName: value.name, - type: value.type, + ...updatedFieldValues, + fieldName: fieldValue.name, + type: fieldValue.type, }); }; + export const clearField = (arrayHelpers, index): void => arrayHelpers.replace(index, {}); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -68,7 +81,13 @@ export function mapFiltersToInitialValues(filters): any[] { case 'EQUALS': return mappedFilters.push({ ...each, - value: each.arguments[0], + value: + each.fieldAttribute.type === 'BOOL' || + each.fieldAttribute?.pduData?.subtype + ? each.arguments[0] + ? 'Yes' + : 'No' + : each.arguments[0], }); case 'CONTAINS': // eslint-disable-next-line no-case-declarations @@ -106,7 +125,12 @@ export function mapCriteriaToInitialValues(criteria) { individualsFiltersBlocks: individualsFiltersBlocks.map((block) => ({ individualBlockFilters: mapFiltersToInitialValues( block.individualBlockFilters, - ), + ).map((filter) => { + return { + ...filter, + isNull: filter.comparisonMethod === 'IS_NULL' || filter.isNull, + }; + }), })), }; } @@ -148,19 +172,61 @@ export function formatCriteriaFilters(filters) { comparisonMethod = 'EQUALS'; values = [each.value]; break; + case 'PDU': + switch ( + each.pduData?.subtype || + each.fieldAttribute?.pduData?.subtype + ) { + case 'SELECT_ONE': + comparisonMethod = 'EQUALS'; + values = [each.value]; + break; + case 'SELECT_MANY': + comparisonMethod = 'CONTAINS'; + values = [...each.value]; + break; + case 'STRING': + comparisonMethod = 'CONTAINS'; + values = [each.value]; + break; + case 'DECIMAL': + case 'INTEGER': + case 'DATE': + if (each.value.from && each.value.to) { + comparisonMethod = 'RANGE'; + values = [each.value.from, each.value.to]; + } else if (each.value.from && !each.value.to) { + comparisonMethod = 'GREATER_THAN'; + values = [each.value.from]; + } else { + comparisonMethod = 'LESS_THAN'; + values = [each.value.to]; + } + break; + case 'BOOL': + comparisonMethod = 'EQUALS'; + values = [each.value === 'Yes']; + break; + default: + comparisonMethod = 'CONTAINS'; + values = [each.value]; + } + break; default: comparisonMethod = 'CONTAINS'; + values = [each.value]; } + return { + ...each, comparisonMethod, arguments: values, fieldName: each.fieldName, - isFlexField: each.isFlexField, fieldAttribute: each.fieldAttribute, + flexFieldClassification: each.flexFieldClassification, }; }); } - // TODO Marcin make Type to this function // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function formatCriteriaIndividualsFiltersBlocks( @@ -171,18 +237,40 @@ export function formatCriteriaIndividualsFiltersBlocks( })); } -function mapFilterToVariable(filter): { +interface Filter { + isNull: boolean; comparisonMethod: string; - arguments; + arguments: any[]; fieldName: string; - isFlexField: boolean; -} { - return { - comparisonMethod: filter.comparisonMethod, - arguments: filter.arguments, + flexFieldClassification: string; + roundNumber?: number; +} + +interface Result { + comparisonMethod: string; + arguments: any[]; + fieldName: string; + flexFieldClassification: string; + roundNumber?: number; +} + +function mapFilterToVariable(filter: Filter): Result { + const result: Result = { + comparisonMethod: filter.isNull ? 'IS_NULL' : filter.comparisonMethod, + arguments: filter.isNull + ? [null] + : filter.arguments.map((arg) => + arg === 'Yes' ? true : arg === 'No' ? false : arg, + ), fieldName: filter.fieldName, - isFlexField: filter.isFlexField, + flexFieldClassification: filter.flexFieldClassification, }; + + if (filter.flexFieldClassification === 'FLEX_FIELD_PDU') { + result.roundNumber = filter.roundNumber; + } + + return result; } // TODO Marcin make Type to this function @@ -207,3 +295,13 @@ export function getTargetingCriteriaVariables(values) { }, }; } + +const flexFieldClassificationMap = { + NOT_FLEX_FIELD: 'Not a Flex Field', + FLEX_FIELD_BASIC: 'Flex Field Basic', + FLEX_FIELD_PDU: 'Flex Field PDU', +}; + +export function mapFlexFieldClassification(key: string): string { + return flexFieldClassificationMap[key] || 'Unknown Classification'; +}