From a13e73021e37e3ae8cf13ea89dd38e650e925301 Mon Sep 17 00:00:00 2001 From: kemar Date: Tue, 5 Nov 2024 11:29:33 +0100 Subject: [PATCH 01/46] Prevent profile bulk update to raise a wrong error when a value is an empty list instead of None --- iaso/api/tasks/create/profiles_bulk_update.py | 16 ++--- iaso/tasks/profiles_bulk_update.py | 68 ++++++++++--------- 2 files changed, 45 insertions(+), 39 deletions(-) diff --git a/iaso/api/tasks/create/profiles_bulk_update.py b/iaso/api/tasks/create/profiles_bulk_update.py index b46751ba48..0477d5dcd8 100644 --- a/iaso/api/tasks/create/profiles_bulk_update.py +++ b/iaso/api/tasks/create/profiles_bulk_update.py @@ -19,15 +19,15 @@ def create(self, request): select_all = request.data.get("select_all", False) selected_ids = request.data.get("selected_ids", []) unselected_ids = request.data.get("unselected_ids", []) - projects_ids_added = request.data.get("projects_ids_added", None) - projects_ids_removed = request.data.get("projects_ids_removed", None) - roles_id_added = request.data.get("roles_id_added", None) - roles_id_removed = request.data.get("roles_id_removed", None) - location_ids_added = request.data.get("location_ids_added", None) - location_ids_removed = request.data.get("location_ids_removed", None) + projects_ids_added = request.data.get("projects_ids_added", []) + projects_ids_removed = request.data.get("projects_ids_removed", []) + roles_id_added = request.data.get("roles_id_added", []) + roles_id_removed = request.data.get("roles_id_removed", []) + location_ids_added = request.data.get("location_ids_added", []) + location_ids_removed = request.data.get("location_ids_removed", []) language = request.data.get("language", None) - teams_id_added = request.data.get("teams_id_added", None) - teams_id_removed = request.data.get("teams_id_removed", None) + teams_id_added = request.data.get("teams_id_added", []) + teams_id_removed = request.data.get("teams_id_removed", []) organization = request.data.get("organization", None) search = request.data.get("search", None) diff --git a/iaso/tasks/profiles_bulk_update.py b/iaso/tasks/profiles_bulk_update.py index 384da42329..afd411ca73 100644 --- a/iaso/tasks/profiles_bulk_update.py +++ b/iaso/tasks/profiles_bulk_update.py @@ -28,14 +28,14 @@ def update_single_profile_from_bulk( managed_org_units: Optional[List[int]], profile: Profile, *, - projects_ids_added: Optional[List[int]], - projects_ids_removed: Optional[List[int]], - roles_id_added: Optional[List[int]], - roles_id_removed: Optional[List[int]], - teams_id_added: Optional[List[int]], - teams_id_removed: Optional[List[int]], - location_ids_added: Optional[List[int]], - location_ids_removed: Optional[List[int]], + projects_ids_added: List[int], + projects_ids_removed: List[int], + roles_id_added: List[int], + roles_id_removed: List[int], + teams_id_added: List[int], + teams_id_removed: List[int], + location_ids_added: List[int], + location_ids_removed: List[int], language: Optional[str], organization: Optional[str], ): @@ -55,14 +55,13 @@ def update_single_profile_from_bulk( user.iaso_profile.get_editable_org_unit_type_ids() if not has_perm_users_admin else set() ) - # Raise if necessary and prepare values to be updated - if teams_id_added is not None: - if not user.has_perm(permission.TEAMS): - raise PermissionDenied(f"User without the permission {permission.TEAMS} cannot add users to team") - if teams_id_removed is not None: - if not user.has_perm(permission.TEAMS): - raise PermissionDenied(f"User without the permission {permission.TEAMS} cannot remove users to team") - if roles_id_added is not None: + if teams_id_added and not user.has_perm(permission.TEAMS): + raise PermissionDenied(f"User without the permission {permission.TEAMS} cannot add users to team") + + if teams_id_removed and not user.has_perm(permission.TEAMS): + raise PermissionDenied(f"User without the permission {permission.TEAMS} cannot remove users to team") + + if roles_id_added: for role_id in roles_id_added: role = get_object_or_404(UserRole, id=role_id, account_id=account_id) if role.account.id == account_id: @@ -70,7 +69,8 @@ def update_single_profile_from_bulk( for p in role.group.permissions.all(): CustomPermissionSupport.assert_right_to_assign(user, p.codename) roles_to_be_added.append(role) - if roles_id_removed is not None: + + if roles_id_removed: for role_id in roles_id_removed: role = get_object_or_404(UserRole, id=role_id, account_id=account_id) if role.account.id == account_id: @@ -79,7 +79,7 @@ def update_single_profile_from_bulk( CustomPermissionSupport.assert_right_to_assign(user, p.codename) roles_to_be_removed.append(role) - if projects_ids_added is not None: + if projects_ids_added: if not has_perm_users_admin: raise PermissionDenied( f"User with permission {permission.USERS_MANAGED} cannot changed project attributions" @@ -88,7 +88,8 @@ def update_single_profile_from_bulk( project = Project.objects.get(pk=project_id) if project.account and project.account.id == account_id: projects_to_be_added.append(project) - if projects_ids_removed is not None: + + if projects_ids_removed: if not has_perm_users_admin: raise PermissionDenied( f"User with permission {permission.USERS_MANAGED} cannot changed project attributions" @@ -119,6 +120,7 @@ def update_single_profile_from_bulk( f"because he does not have rights on the following org unit type: {org_unit.org_unit_type.name}" ) org_units_to_be_added.append(org_unit) + if location_ids_removed: for location_id in location_ids_removed: if managed_org_units and (not has_perm_users_admin) and (location_id not in managed_org_units): @@ -140,8 +142,9 @@ def update_single_profile_from_bulk( f"because he does not have rights on the following org unit type: {org_unit.org_unit_type.name}" ) org_units_to_be_removed.append(org_unit) + # Update - if teams_id_added is not None: + if teams_id_added: team_audit_logger = TeamAuditLogger() for team_id in teams_id_added: team = Team.objects.get(pk=team_id) @@ -153,7 +156,8 @@ def update_single_profile_from_bulk( ): team.users.add(profile.user) team_audit_logger.log_modification(instance=team, old_data_dump=old_team, request_user=user) - if teams_id_removed is not None: + + if teams_id_removed: team_audit_logger = TeamAuditLogger() for team_id in teams_id_removed: team = Team.objects.get(pk=team_id) @@ -165,9 +169,11 @@ def update_single_profile_from_bulk( ): team.users.remove(profile.user) team_audit_logger.log_modification(instance=team, old_data_dump=old_team, request_user=user) - if language is not None: + + if language: profile.language = language - if organization is not None: + + if organization: profile.organization = organization if len(roles_to_be_added) > 0: @@ -195,14 +201,14 @@ def profiles_bulk_update( select_all: bool, selected_ids: List[int], unselected_ids: List[int], - projects_ids_added: Optional[List[int]], - projects_ids_removed: Optional[List[int]], - roles_id_added: Optional[List[int]], - roles_id_removed: Optional[List[int]], - teams_id_added: Optional[List[int]], - teams_id_removed: Optional[List[int]], - location_ids_added: Optional[List[int]], - location_ids_removed: Optional[List[int]], + projects_ids_added: List[int], + projects_ids_removed: List[int], + roles_id_added: List[int], + roles_id_removed: List[int], + teams_id_added: List[int], + teams_id_removed: List[int], + location_ids_added: List[int], + location_ids_removed: List[int], language: Optional[str], organization: Optional[str], search: Optional[str], From 8a26141f2deb263b27b7e1b8dbe0c5d4bb578957 Mon Sep 17 00:00:00 2001 From: kemar Date: Tue, 5 Nov 2024 16:42:51 +0100 Subject: [PATCH 02/46] Use full payload in some tests --- iaso/api/tasks/create/profiles_bulk_update.py | 20 +- iaso/tests/api/test_profiles_bulk_update.py | 181 +++++++++++++++--- 2 files changed, 166 insertions(+), 35 deletions(-) diff --git a/iaso/api/tasks/create/profiles_bulk_update.py b/iaso/api/tasks/create/profiles_bulk_update.py index 0477d5dcd8..a1dcbe8fe3 100644 --- a/iaso/api/tasks/create/profiles_bulk_update.py +++ b/iaso/api/tasks/create/profiles_bulk_update.py @@ -17,17 +17,17 @@ class ProfilesBulkUpdate(viewsets.ViewSet): def create(self, request): select_all = request.data.get("select_all", False) - selected_ids = request.data.get("selected_ids", []) - unselected_ids = request.data.get("unselected_ids", []) - projects_ids_added = request.data.get("projects_ids_added", []) - projects_ids_removed = request.data.get("projects_ids_removed", []) - roles_id_added = request.data.get("roles_id_added", []) - roles_id_removed = request.data.get("roles_id_removed", []) - location_ids_added = request.data.get("location_ids_added", []) - location_ids_removed = request.data.get("location_ids_removed", []) + selected_ids = request.data.get("selected_ids") or [] + unselected_ids = request.data.get("unselected_ids") or [] + projects_ids_added = request.data.get("projects_ids_added") or [] + projects_ids_removed = request.data.get("projects_ids_removed") or [] + roles_id_added = request.data.get("roles_id_added") or [] + roles_id_removed = request.data.get("roles_id_removed") or [] + location_ids_added = request.data.get("location_ids_added") or [] + location_ids_removed = request.data.get("location_ids_removed") or [] language = request.data.get("language", None) - teams_id_added = request.data.get("teams_id_added", []) - teams_id_removed = request.data.get("teams_id_removed", []) + teams_id_added = request.data.get("teams_id_added") or [] + teams_id_removed = request.data.get("teams_id_removed") or [] organization = request.data.get("organization", None) search = request.data.get("search", None) diff --git a/iaso/tests/api/test_profiles_bulk_update.py b/iaso/tests/api/test_profiles_bulk_update.py index 60cfd95dd2..fe7723fab1 100644 --- a/iaso/tests/api/test_profiles_bulk_update.py +++ b/iaso/tests/api/test_profiles_bulk_update.py @@ -228,9 +228,7 @@ def test_profile_bulkupdate_select_some(self): operation_payload = { "select_all": False, "selected_ids": [self.luke.iaso_profile.pk, self.chewie.iaso_profile.pk], - "language": "fr", - "location_ids_added": [self.jedi_council_corruscant.pk], - "location_ids_removed": [self.jedi_council_endor.pk], + "unselected_ids": None, "projects_ids_added": [ self.project.pk, self.project_3.pk, @@ -238,7 +236,20 @@ def test_profile_bulkupdate_select_some(self): "projects_ids_removed": [self.project_2.pk], "roles_id_added": [self.user_role.pk], "roles_id_removed": [self.user_role_2.pk], + "location_ids_added": [self.jedi_council_corruscant.pk], + "location_ids_removed": [self.jedi_council_endor.pk], + "language": "fr", + "teams_id_added": None, + "teams_id_removed": None, "organization": "Bluesquare", + "search": None, + "perms": None, + "location": None, + "org_unit_type": None, + "parent_ou": None, + "children_ou": None, + "projects": None, + "user_roles": None, } response = self.client.post(f"/api/tasks/create/profilesbulkupdate/", data=operation_payload, format="json") @@ -333,14 +344,25 @@ def test_profile_bulkupdate_should_fail_with_restricted_editable_org_unit_types( payload = { "select_all": False, "selected_ids": [self.luke.iaso_profile.pk, self.chewie.iaso_profile.pk], - "language": "fr", + "unselected_ids": [], + "projects_ids_added": [], + "projects_ids_removed": [], + "roles_id_added": [], + "roles_id_removed": [self.user_role_2.pk], "location_ids_added": [self.jedi_council_endor.pk], - "location_ids_removed": None, - "projects_ids_added": None, - "projects_ids_removed": None, - "roles_id_added": None, - "roles_id_removed": None, + "location_ids_removed": [], + "language": "fr", + "teams_id_added": [], + "location_ids_removed": [], "organization": "Bluesquare", + "search": None, + "perms": None, + "location": None, + "org_unit_type": None, + "parent_ou": None, + "children_ou": None, + "projects": None, + "user_roles": None, } response = self.client.post(f"/api/tasks/create/profilesbulkupdate/", data=payload, format="json") @@ -405,9 +427,26 @@ def test_profile_bulkupdate_user_managed_can_add_role(self): self.client.force_authenticate(self.obi_wan) operation_payload = { "select_all": True, - "roles_id_added": [ - self.user_role.pk, - ], + "selected_ids": [], + "unselected_ids": [], + "projects_ids_added": [], + "projects_ids_removed": [], + "roles_id_added": [self.user_role.pk], + "roles_id_removed": [], + "location_ids_added": [], + "location_ids_removed": [], + "language": None, + "teams_id_added": [], + "teams_id_removed": [], + "organization": None, + "search": None, + "perms": None, + "location": None, + "org_unit_type": None, + "parent_ou": None, + "children_ou": None, + "projects": None, + "user_roles": None, } response = self.client.post(f"/api/tasks/create/profilesbulkupdate/", data=operation_payload, format="json") @@ -435,9 +474,26 @@ def test_profile_bulkupdate_user_managed_cannot_add_role_with_admin_permission(s self.client.force_authenticate(self.obi_wan) operation_payload = { "select_all": True, - "roles_id_added": [ - self.user_role_admin.pk, - ], + "selected_ids": [], + "unselected_ids": [], + "projects_ids_added": [], + "projects_ids_removed": [], + "roles_id_added": [self.user_role_admin.pk], + "roles_id_removed": [], + "location_ids_added": [], + "location_ids_removed": [], + "language": None, + "teams_id_added": [], + "teams_id_removed": [], + "organization": None, + "search": None, + "perms": None, + "location": None, + "org_unit_type": None, + "parent_ou": None, + "children_ou": None, + "projects": None, + "user_roles": None, } response = self.client.post(f"/api/tasks/create/profilesbulkupdate/", data=operation_payload, format="json") @@ -465,9 +521,26 @@ def test_profile_bulkupdate_user_managed_can_remove_role(self): self.client.force_authenticate(self.obi_wan) operation_payload = { "select_all": True, - "roles_id_removed": [ - self.user_role_2.pk, - ], + "selected_ids": [], + "unselected_ids": [], + "projects_ids_added": [], + "projects_ids_removed": [], + "roles_id_added": [], + "roles_id_removed": [self.user_role_2.pk], + "location_ids_added": [], + "location_ids_removed": [], + "language": None, + "teams_id_added": [], + "teams_id_removed": [], + "organization": None, + "search": None, + "perms": None, + "location": None, + "org_unit_type": None, + "parent_ou": None, + "children_ou": None, + "projects": None, + "user_roles": None, } response = self.client.post(f"/api/tasks/create/profilesbulkupdate/", data=operation_payload, format="json") @@ -497,10 +570,25 @@ def test_profile_bulkupdate_add_user_role_with_not_connected_account(self): operation_payload = { "select_all": False, "selected_ids": [self.luke.iaso_profile.pk, self.chewie.iaso_profile.pk], + "unselected_ids": [], + "projects_ids_added": [], + "projects_ids_removed": [], + "roles_id_added": [self.user_role_different_account.pk], + "roles_id_removed": [], + "location_ids_added": [], + "location_ids_removed": [], "language": "fr", - "roles_id_added": [ - self.user_role_different_account.pk, - ], + "teams_id_added": [], + "teams_id_removed": [], + "organization": None, + "search": None, + "perms": None, + "location": None, + "org_unit_type": None, + "parent_ou": None, + "children_ou": None, + "projects": None, + "user_roles": None, } response = self.client.post(f"/api/tasks/create/profilesbulkupdate/", data=operation_payload, format="json") @@ -524,10 +612,27 @@ def test_profile_bulkupdate_remove_user_role_with_not_connected_account(self): operation_payload = { "select_all": False, "selected_ids": [self.luke.iaso_profile.pk, self.chewie.iaso_profile.pk], - "language": "fr", + "unselected_ids": [], + "projects_ids_added": [], + "projects_ids_removed": [], + "roles_id_added": [], "roles_id_removed": [ self.user_role_different_account.pk, ], + "location_ids_added": [], + "location_ids_removed": [], + "language": "fr", + "teams_id_added": [], + "teams_id_removed": [], + "organization": None, + "search": None, + "perms": None, + "location": None, + "org_unit_type": None, + "parent_ou": None, + "children_ou": None, + "projects": None, + "user_roles": None, } response = self.client.post(f"/api/tasks/create/profilesbulkupdate/", data=operation_payload, format="json") @@ -550,7 +655,29 @@ def test_profile_bulkupdate_select_all(self): self.client.force_authenticate(self.yoda) response = self.client.post( f"/api/tasks/create/profilesbulkupdate/", - data={"select_all": True, "language": "fr"}, + data={ + "select_all": True, + "selected_ids": None, + "unselected_ids": None, + "projects_ids_added": None, + "projects_ids_removed": None, + "roles_id_added": None, + "roles_id_removed": None, + "location_ids_added": None, + "location_ids_removed": None, + "language": "fr", + "teams_id_added": None, + "teams_id_removed": None, + "organization": None, + "search": None, + "perms": None, + "location": None, + "org_unit_type": None, + "parent_ou": None, + "children_ou": None, + "projects": None, + "user_roles": None, + }, format="json", ) @@ -661,7 +788,11 @@ def test_profile_bulkupdate_task_select_all_but_some(self): @tag("iaso_only") def test_profile_bulkupdate_user_without_iaso_team_permission(self): """POST /api/tasks/create/profilesbulkupdate/ a user without permission menupermissions.iaso_teams cannot add users to team""" - self.client.force_authenticate(self.obi_wan) + user = self.obi_wan + self.assertFalse(user.has_perm(permission.TEAMS)) + + self.client.force_authenticate(user) + operation_payload = { "select_all": True, "teams_id_added": [ @@ -673,7 +804,7 @@ def test_profile_bulkupdate_user_without_iaso_team_permission(self): self.assertJSONResponse(response, 201) data = response.json() task = self.assertValidTaskAndInDB(data["task"], status="QUEUED", name="profiles_bulk_update") - self.assertEqual(task.launcher, self.obi_wan) + self.assertEqual(task.launcher, user) # Run the task self.runAndValidateTask(task, "ERRORED") From 676e8bb06272f7ddbcd90eba5af8fa4be254a5cb Mon Sep 17 00:00:00 2001 From: kemar Date: Wed, 6 Nov 2024 17:10:27 +0100 Subject: [PATCH 03/46] Allow to set Org Unit Type restrictions in bulk user creation --- .../fixtures/sample_bulk_user_creation.csv | 4 +- iaso/api/profiles/bulk_create_users.py | 52 ++++++++--- iaso/api/profiles/profiles.py | 4 +- iaso/tests/api/test_profiles.py | 33 ++++--- .../test_user_bulk_create_all_fields.csv | 8 +- ...er_bulk_create_creator_no_access_to_ou.csv | 4 +- ...st_user_bulk_create_duplicated_ou_name.csv | 8 +- .../test_user_bulk_create_invalid_mail.csv | 8 +- .../test_user_bulk_create_invalid_orgunit.csv | 8 +- .../test_user_bulk_create_invalid_ou_name.csv | 8 +- ...test_user_bulk_create_invalid_password.csv | 8 +- ...est_user_bulk_create_managed_geo_limit.csv | 8 +- .../test_user_bulk_create_no_mail.csv | 8 +- .../test_user_bulk_create_semicolon.csv | 6 +- ...er_bulk_create_user_access_to_child_ou.csv | 4 +- ...er_bulk_create_user_duplicate_ou_names.csv | 4 +- .../fixtures/test_user_bulk_create_valid.csv | 8 +- .../test_user_bulk_create_valid_with_perm.csv | 8 +- ...t_user_bulk_create_valid_with_projects.csv | 6 +- ...test_user_bulk_create_valid_with_roles.csv | 6 +- .../test_user_bulk_missing_columns.csv | 4 +- iaso/tests/test_create_users_from_csv.py | 88 +++++++++++++++++-- 22 files changed, 203 insertions(+), 92 deletions(-) diff --git a/iaso/api/fixtures/sample_bulk_user_creation.csv b/iaso/api/fixtures/sample_bulk_user_creation.csv index 66572114aa..1ef4e19264 100644 --- a/iaso/api/fixtures/sample_bulk_user_creation.csv +++ b/iaso/api/fixtures/sample_bulk_user_creation.csv @@ -1,2 +1,2 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,Organization,projects,user_roles,phone_number -user name should not contain whitespaces,"Min. 8 characters, should include 1 letter and 1 number",,,,Use Org Unit ID to avoid errors,Org Unit external ID,"Possible values: iaso_forms,iaso_mappings,iaso_completeness,iaso_org_units,iaso_links,iaso_users,iaso_pages,iaso_projects,iaso_sources,iaso_data_tasks,iaso_submissions,iaso_update_submission,iaso_planning,iaso_reports,iaso_teams,iaso_assignments,iaso_entities,iaso_storages,iaso_completeness_stats,iaso_workflows,iaso_registry","Possible values: EN, FR",Optional,Optional,projects,user roles,The phone number as a string (in single or double quote). It has to start with the country code like +1 for US +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,Organization,projects,user_roles,phone_number,editable_org_unit_types +user name should not contain whitespaces,"Min. 8 characters, should include 1 letter and 1 number",,,,Use Org Unit ID to avoid errors,Org Unit external ID,"Possible values: iaso_forms,iaso_mappings,iaso_completeness,iaso_org_units,iaso_links,iaso_users,iaso_pages,iaso_projects,iaso_sources,iaso_data_tasks,iaso_submissions,iaso_update_submission,iaso_planning,iaso_reports,iaso_teams,iaso_assignments,iaso_entities,iaso_storages,iaso_completeness_stats,iaso_workflows,iaso_registry","Possible values: EN, FR",Optional,Optional,projects,user roles,The phone number as a string (in single or double quote). It has to start with the country code like +1 for US,"Use comma separated Org Unit Type IDs to avoid errors: 1, 2" diff --git a/iaso/api/profiles/bulk_create_users.py b/iaso/api/profiles/bulk_create_users.py index 2c340c9e0f..57422faf91 100644 --- a/iaso/api/profiles/bulk_create_users.py +++ b/iaso/api/profiles/bulk_create_users.py @@ -40,6 +40,7 @@ "user_roles", "projects", "phone_number", + "editable_org_unit_types", ] @@ -52,10 +53,9 @@ class Meta: class HasUserPermission(permissions.BasePermission): def has_permission(self, request, view): - if not (request.user.has_perm(permission.USERS_ADMIN) or request.user.has_perm(permission.USERS_MANAGED)): - return False - - return True + if request.user.has_perm(permission.USERS_ADMIN) or request.user.has_perm(permission.USERS_MANAGED): + return True + return False class BulkCreateUserFromCsvViewSet(ModelViewSet): @@ -134,14 +134,7 @@ def create(self, request, *args, **kwargs): user_csv = request.FILES["file"] user_csv_decoded = user_csv.read().decode("utf-8") csv_str = io.StringIO(user_csv_decoded) - - try: - delimiter = csv.Sniffer().sniff(user_csv_decoded).delimiter - except csv.Error: - try: - delimiter = ";" if ";" in user_csv.decoded else "," - except Exception: - raise serializers.ValidationError({"error": "Error : CSV File incorrectly formatted."}) + delimiter = ";" if ";" in user_csv_decoded else "," reader = csv.reader(csv_str, delimiter=delimiter) """In case the delimiter is " ; " we must ensure that the multiple value can be read so we replace it with a " * " instead of " , " """ @@ -422,6 +415,41 @@ def create(self, request, *args, **kwargs): phone_number = row[csv_indexes.index("phone_number")] profile.phone_number = self.validate_phone_number(phone_number) + try: + editable_org_unit_types_ids = row[csv_indexes.index("editable_org_unit_types")] + editable_org_unit_types_ids = ( + editable_org_unit_types_ids.split(",") if editable_org_unit_types_ids else None + ) + except (IndexError, ValueError): + editable_org_unit_types_ids = None + + if editable_org_unit_types_ids: + new_editable_org_unit_types = OrgUnitType.objects.filter( + projects__account=importer_account, id__in=editable_org_unit_types_ids + ) + if new_editable_org_unit_types: + if user_editable_org_unit_type_ids: + invalid_ids = [ + out.pk + for out in new_editable_org_unit_types + if not profile.has_org_unit_write_permission( + out.pk, user_editable_org_unit_type_ids + ) + ] + if invalid_ids: + invalid_names = ", ".join( + name + for name in OrgUnitType.objects.filter(pk__in=invalid_ids).values_list( + "name", flat=True + ) + ) + raise serializers.ValidationError( + { + "error": f"Operation aborted. You don't have rights on the following org unit types: {invalid_names}" + } + ) + profile.editable_org_unit_types.set(new_editable_org_unit_types) + profile.org_units.set(org_units_list) # link the auth user to the user role corresponding auth group profile.user.groups.set(user_groups_list) diff --git a/iaso/api/profiles/profiles.py b/iaso/api/profiles/profiles.py index 710cb12a23..498be27d88 100644 --- a/iaso/api/profiles/profiles.py +++ b/iaso/api/profiles/profiles.py @@ -583,7 +583,8 @@ def list_export( columns = [{"title": column} for column in BULK_CREATE_USER_COLUMNS_LIST] def get_row(profile: Profile, **_) -> List[Any]: - org_units = profile.org_units.all().order_by("id") + org_units = profile.org_units.order_by("id").only("id", "source_ref") + editable_org_unit_types_pks = profile.editable_org_unit_types.order_by("id").values_list("id", flat=True) return [ profile.user.username, @@ -603,6 +604,7 @@ def get_row(profile: Profile, **_) -> List[Any]: ), ",".join(str(item.name) for item in profile.projects.all().order_by("id")), (f"'{profile.phone_number}'" if profile.phone_number else None), + ",".join(str(pk) for pk in editable_org_unit_types_pks), ] filename = "users" diff --git a/iaso/tests/api/test_profiles.py b/iaso/tests/api/test_profiles.py index 6d767c2fc7..20a45ec407 100644 --- a/iaso/tests/api/test_profiles.py +++ b/iaso/tests/api/test_profiles.py @@ -134,9 +134,6 @@ def setUpTestData(cls): cls.jedi_council = m.OrgUnitType.objects.create(name="Jedi Council", short_name="Cnc") cls.jedi_council.sub_unit_types.add(cls.jedi_squad) - # cls.mock_multipolygon = MultiPolygon(Polygon([[-1.3, 2.5], [-1.7, 2.8], [-1.1, 4.1], [-1.3, 2.5]])) - # cls.mock_point = Point(x=4, y=50, z=100) - cls.mock_multipolygon = None cls.mock_point = None @@ -317,6 +314,8 @@ def test_profile_list_ok(self): def test_profile_list_export_as_csv(self): self.john.iaso_profile.org_units.set([self.jedi_squad_1, self.jedi_council_corruscant]) + self.jum.iaso_profile.editable_org_unit_types.set([self.jedi_squad]) + self.client.force_authenticate(self.jane) response = self.client.get("/api/profiles/?csv=true") self.assertEqual(response.status_code, 200) @@ -338,21 +337,23 @@ def test_profile_list_export_as_csv(self): "permissions," "user_roles," "projects," - "phone_number\r\n" + "phone_number," + "editable_org_unit_types\r\n" ) - expected_csv += "janedoe,,,,,,,,,,iaso_forms,,,\r\n" - expected_csv += f'johndoe,,,,,"{self.jedi_squad_1.pk},{self.jedi_council_corruscant.pk}",{self.jedi_council_corruscant.source_ref},,,,,,,\r\n' - expected_csv += 'jim,,,,,,,,,,"iaso_forms,iaso_users",,,\r\n' - expected_csv += "jam,,,,,,,en,,,iaso_users_managed,,,\r\n" - expected_csv += "jom,,,,,,,fr,,,,,,\r\n" - expected_csv += f"jum,,,,,,,,,,,,{self.project.name},\r\n" - expected_csv += f'managedGeoLimit,,,,,{self.jedi_council_corruscant.id},{self.jedi_council_corruscant.source_ref},,,,iaso_users_managed,"{self.user_role_name},{self.user_role_another_account_name}",,\r\n' + expected_csv += "janedoe,,,,,,,,,,iaso_forms,,,,\r\n" + expected_csv += f'johndoe,,,,,"{self.jedi_squad_1.pk},{self.jedi_council_corruscant.pk}",{self.jedi_council_corruscant.source_ref},,,,,,,,\r\n' + expected_csv += 'jim,,,,,,,,,,"iaso_forms,iaso_users",,,,\r\n' + expected_csv += "jam,,,,,,,en,,,iaso_users_managed,,,,\r\n" + expected_csv += "jom,,,,,,,fr,,,,,,,\r\n" + expected_csv += f"jum,,,,,,,,,,,,{self.project.name},,{self.jedi_squad.pk}\r\n" + expected_csv += f'managedGeoLimit,,,,,{self.jedi_council_corruscant.id},{self.jedi_council_corruscant.source_ref},,,,iaso_users_managed,"{self.user_role_name},{self.user_role_another_account_name}",,,\r\n' self.assertEqual(response_csv, expected_csv) def test_profile_list_export_as_xlsx(self): self.john.iaso_profile.org_units.set([self.jedi_squad_1, self.jedi_council_corruscant]) + self.jum.iaso_profile.editable_org_unit_types.set([self.jedi_squad]) self.client.force_authenticate(self.jane) response = self.client.get("/api/profiles/?xlsx=true") @@ -379,6 +380,7 @@ def test_profile_list_export_as_xlsx(self): "user_roles", "projects", "phone_number", + "editable_org_unit_types", ], ) @@ -433,6 +435,15 @@ def test_profile_list_export_as_xlsx(self): }, "projects": {0: None, 1: None, 2: None, 3: None, 4: None, 5: self.project.name, 6: None}, "phone_number": {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None}, + "editable_org_unit_types": { + 0: None, + 1: None, + 2: None, + 3: None, + 4: None, + 5: self.jedi_squad.pk, + 6: None, + }, }, ) diff --git a/iaso/tests/fixtures/test_user_bulk_create_all_fields.csv b/iaso/tests/fixtures/test_user_bulk_create_all_fields.csv index 9933022ab9..fa01b959bd 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_all_fields.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_all_fields.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,user_roles,projects,phone_number -bob,yodnj!30dln,bob@bluesquarehub.com,Bob,bio,,baz,fr,dhis2_id_1,,"iaso_forms, iaso_submissions","manager, area_manager",Project name, -bobette,!dsgd39hfghd,bobette@bluesquarehub.com,Bobette,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",baz,fr,dhis2_id_2,,,,Project name, -fanchon,frdghb30cd!hd,fanchon@srgrs.com,Fanchon,dsfsf,"Tatooine, Dagobah",,FR,dhis2_id_3,,iaso_forms,manager,Project name, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,user_roles,projects,phone_number,editable_org_unit_types +bob,yodnj!30dln,bob@bluesquarehub.com,Bob,bio,,baz,fr,dhis2_id_1,,"iaso_forms, iaso_submissions","manager, area_manager",Project name,, +bobette,!dsgd39hfghd,bobette@bluesquarehub.com,Bobette,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",baz,fr,dhis2_id_2,,,,Project name,, +fanchon,frdghb30cd!hd,fanchon@srgrs.com,Fanchon,dsfsf,"Tatooine, Dagobah",,FR,dhis2_id_3,,iaso_forms,manager,Project name,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_creator_no_access_to_ou.csv b/iaso/tests/fixtures/test_user_bulk_create_creator_no_access_to_ou.csv index 369f69483a..b06d3cdc7f 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_creator_no_access_to_ou.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_creator_no_access_to_ou.csv @@ -1,2 +1,2 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,projects,permissions,user_roles,phone_number -jan,yodnj!30dln,janvier@bluesquarehub.com,jan,vier,10244,,fr,dhis2_id_1,,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,projects,permissions,user_roles,phone_number,editable_org_unit_types +jan,yodnj!30dln,janvier@bluesquarehub.com,jan,vier,10244,,fr,dhis2_id_1,,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_duplicated_ou_name.csv b/iaso/tests/fixtures/test_user_bulk_create_duplicated_ou_name.csv index d4b80940e6..53e90abc44 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_duplicated_ou_name.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_duplicated_ou_name.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,organization,user_roles,projects -broly,yodnjdln,biobroly@bluesquarehub.com,broly,bio,9999,,FR,3936393,,,, -ferdinand,dsgdhf73!ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,fr,23762429,,, -rsfg,fr783!fDd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Solana",,,FR,3783937,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,organization,user_roles,projects,editable_org_unit_types +broly,yodnjdln,biobroly@bluesquarehub.com,broly,bio,9999,,FR,3936393,,,,, +ferdinand,dsgdhf73!ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,fr,23762429,,,, +rsfg,fr783!fDd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Solana",,,FR,3783937,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_invalid_mail.csv b/iaso/tests/fixtures/test_user_bulk_create_invalid_mail.csv index 402476051e..7c747f0474 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_invalid_mail.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_invalid_mail.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number -broly,yOdnjdln8!,biobroly@bluesquarehub.com,broly,bio,9999,,efsd44,,fr,,,, -ferdinand,dEF80s:ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,73JJ37,,fr,,,, -rsfg,frdgEF73f9!,efsfg@srgrscom,feesf,dsfsf,"Tatooine, Dagobah",,N363B8D,,FR,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number,editable_org_unit_types +broly,yOdnjdln8!,biobroly@bluesquarehub.com,broly,bio,9999,,efsd44,,fr,,,,, +ferdinand,dEF80s:ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,73JJ37,,fr,,,,, +rsfg,frdgEF73f9!,efsfg@srgrscom,feesf,dsfsf,"Tatooine, Dagobah",,N363B8D,,FR,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_invalid_orgunit.csv b/iaso/tests/fixtures/test_user_bulk_create_invalid_orgunit.csv index cec5d55b0c..dcf9ead5d5 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_invalid_orgunit.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_invalid_orgunit.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,user_roles,projects,phone_number,phone_number -broly,yodnj!d93ln,biobroly@bluesquarehub.com,broly,bio,99998,,fr,,,,,,, -ferdinand,ds!809gdhfghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",nfr,,,,,,,, -rsfg,frdghb803!dhd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah",nFR,,,,,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,user_roles,projects,phone_number,phone_number,editable_org_unit_types +broly,yodnj!d93ln,biobroly@bluesquarehub.com,broly,bio,99998,,fr,,,,,,,, +ferdinand,ds!809gdhfghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",nfr,,,,,,,,, +rsfg,frdghb803!dhd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah",nFR,,,,,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_invalid_ou_name.csv b/iaso/tests/fixtures/test_user_bulk_create_invalid_ou_name.csv index a5780aa1af..2dd7eb30c0 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_invalid_ou_name.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_invalid_ou_name.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number -broly,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,9999,,,,fr,,,, -ferdinand,!dsgd39hfghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,,fr,,,, -rsfg,frdghb30cd!hd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah, Bazarre",,,,FR,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number,editable_org_unit_types +broly,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,9999,,,,fr,,,,, +ferdinand,!dsgd39hfghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,,fr,,,,, +rsfg,frdghb30cd!hd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah, Bazarre",,,,FR,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_invalid_password.csv b/iaso/tests/fixtures/test_user_bulk_create_invalid_password.csv index 6d17190cb7..b12264d3ff 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_invalid_password.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_invalid_password.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number -broly,yodnjdln,biobroly@bluesquarehub.com,broly,bio,9999,,,,fr,,,, -ferdinand,dsgdhf73!ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,,fr,,,, -rsfg,frd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah",,,,FR,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number,editable_org_unit_types +broly,yodnjdln,biobroly@bluesquarehub.com,broly,bio,9999,,,,fr,,,,, +ferdinand,dsgdhf73!ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,,fr,,,,, +rsfg,frd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah",,,,FR,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_managed_geo_limit.csv b/iaso/tests/fixtures/test_user_bulk_create_managed_geo_limit.csv index 08d4761acc..84acec0b65 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_managed_geo_limit.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_managed_geo_limit.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number -broly,yOdnjdln8!,biobroly@bluesquarehub.com,broly,bio,1112,,,,fr,,,, -ferdinand,dEF80s:ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,1112,,,,fr,,,, -rsfg,frdgEF73f9!,,feesf,dsfsf,1112,,,,FR,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number,editable_org_unit_types +broly,yOdnjdln8!,biobroly@bluesquarehub.com,broly,bio,1112,,,,fr,,,,, +ferdinand,dEF80s:ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,1112,,,,fr,,,,, +rsfg,frdgEF73f9!,,feesf,dsfsf,1112,,,,FR,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_no_mail.csv b/iaso/tests/fixtures/test_user_bulk_create_no_mail.csv index e5f40b88ca..737c89317b 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_no_mail.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_no_mail.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number -broly,yOdnjdln8!,biobroly@bluesquarehub.com,broly,bio,9999,,,,fr,,,, -ferdinand,dEF80s:ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,,fr,,,, -rsfg,frdgEF73f9!,,feesf,dsfsf,"Tatooine, Dagobah",,,,FR,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,dhis2_id,organization,profile_language,permissions,user_roles,projects,phone_number,editable_org_unit_types +broly,yOdnjdln8!,biobroly@bluesquarehub.com,broly,bio,9999,,,,fr,,,,, +ferdinand,dEF80s:ghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,,fr,,,,, +rsfg,frdgEF73f9!,,feesf,dsfsf,"Tatooine, Dagobah",,,,FR,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_semicolon.csv b/iaso/tests/fixtures/test_user_bulk_create_semicolon.csv index 43b42d5f61..2d1ba04713 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_semicolon.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_semicolon.csv @@ -1,3 +1,3 @@ -username;password;email;first_name;last_name;orgunit;orgunit__source_ref;profile_language;dhis2_id;organization;permissions;user_roles;projects;phone_number -broly;yodnj!30dln;biobroly@bluesquarehub.com;broly;bio;9999;;fr;dhis2_id_1;;;;; -cyrus;yodnj!30dln;cyruswashington@bluesquarehub.com;cyrus;washington;;;fr;dhis2_id_6;;;;; +username;password;email;first_name;last_name;orgunit;orgunit__source_ref;profile_language;dhis2_id;organization;permissions;user_roles;projects;phone_number;editable_org_unit_types +broly;yodnj!30dln;biobroly@bluesquarehub.com;broly;bio;9999;;fr;dhis2_id_1;;;;;; +cyrus;yodnj!30dln;cyruswashington@bluesquarehub.com;cyrus;washington;;;fr;dhis2_id_6;;;;;; diff --git a/iaso/tests/fixtures/test_user_bulk_create_user_access_to_child_ou.csv b/iaso/tests/fixtures/test_user_bulk_create_user_access_to_child_ou.csv index 86d3f4de80..178db475a3 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_user_access_to_child_ou.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_user_access_to_child_ou.csv @@ -1,2 +1,2 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,projects,user_roles,phone_number -jan,yodnj!30dln,janvier@bluesquarehub.com,jan,vier,10244,,fr,dhis2_id_1,,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,projects,user_roles,phone_number,editable_org_unit_types +jan,yodnj!30dln,janvier@bluesquarehub.com,jan,vier,10244,,fr,dhis2_id_1,,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_user_duplicate_ou_names.csv b/iaso/tests/fixtures/test_user_bulk_create_user_duplicate_ou_names.csv index 82f6b2f19f..4362c8c9ae 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_user_duplicate_ou_names.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_user_duplicate_ou_names.csv @@ -1,2 +1,2 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,projects,permissions,user_roles,phone_number -jan,yodnj!30dln,janvier@bluesquarehub.com,jan,vier,chiloe,,fr,dhis2_id_1,,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,projects,permissions,user_roles,phone_number,editable_org_unit_types +jan,yodnj!30dln,janvier@bluesquarehub.com,jan,vier,chiloe,,fr,dhis2_id_1,,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_valid.csv b/iaso/tests/fixtures/test_user_bulk_create_valid.csv index 3d9680a6a6..4a9b195807 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_valid.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_valid.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,user_roles,projects,phone_number -broly,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,,baz,fr,dhis2_id_1,,,,, -ferdinand,!dsgd39hfghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",baz,fr,dhis2_id_2,,,,, -rsfg,frdghb30cd!hd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah",,FR,dhis2_id_3,,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,user_roles,projects,phone_number,editable_org_unit_types +broly,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,,baz,fr,dhis2_id_1,,,,,, +ferdinand,!dsgd39hfghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",baz,fr,dhis2_id_2,,,,,, +rsfg,frdghb30cd!hd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah",,FR,dhis2_id_3,,,,,,1 diff --git a/iaso/tests/fixtures/test_user_bulk_create_valid_with_perm.csv b/iaso/tests/fixtures/test_user_bulk_create_valid_with_perm.csv index 9b6ca66ac0..d6f7c15561 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_valid_with_perm.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_valid_with_perm.csv @@ -1,4 +1,4 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,organization,user_roles,projects,phone_number -pollux,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,9999,,"iaso_forms, iaso_submissions",EN,KBFZK83J,,,, -ferdinand,!dsgd39hfghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,fr,9383,,,, -castor,frdghb30cd!hd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah",,iaso_projects,EN,SKNF83NF93,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,organization,user_roles,projects,phone_number,editable_org_unit_types +pollux,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,9999,,"iaso_forms, iaso_submissions",EN,KBFZK83J,,,,, +ferdinand,!dsgd39hfghd,fdzepplin@bluesquarehub.com,ferdinand,von Zepplin,"Coruscant Jedi Council, 9999, Tatooine",,,fr,9383,,,,, +castor,frdghb30cd!hd,efsfg@srgrs.com,feesf,dsfsf,"Tatooine, Dagobah",,iaso_projects,EN,SKNF83NF93,,,,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_valid_with_projects.csv b/iaso/tests/fixtures/test_user_bulk_create_valid_with_projects.csv index 2accc657ae..f8f3196288 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_valid_with_projects.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_valid_with_projects.csv @@ -1,3 +1,3 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,user_roles,projects,phone_number -broly,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,9999,,fr,dhis2_id_1,,,,Project name, -cyrus,yodnj!30dln,cyruswashington@bluesquarehub.com,cyrus,washington,,,fr,dhis2_id_6,,,,Project name, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,user_roles,projects,phone_number,editable_org_unit_types +broly,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,9999,,fr,dhis2_id_1,,,,Project name,, +cyrus,yodnj!30dln,cyruswashington@bluesquarehub.com,cyrus,washington,,,fr,dhis2_id_6,,,,Project name,, diff --git a/iaso/tests/fixtures/test_user_bulk_create_valid_with_roles.csv b/iaso/tests/fixtures/test_user_bulk_create_valid_with_roles.csv index 398a689f45..628e31d917 100644 --- a/iaso/tests/fixtures/test_user_bulk_create_valid_with_roles.csv +++ b/iaso/tests/fixtures/test_user_bulk_create_valid_with_roles.csv @@ -1,3 +1,3 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,projects,user_roles,phone_number -broly,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,9999,,fr,dhis2_id_1,,,,manager, -cyrus,yodnj!30dln,cyruswashington@bluesquarehub.com,cyrus,washington,,,fr,dhis2_id_6,,,,"manager, area_manager", +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,permissions,projects,user_roles,phone_number,editable_org_unit_types +broly,yodnj!30dln,biobroly@bluesquarehub.com,broly,bio,9999,,fr,dhis2_id_1,,,,manager,, +cyrus,yodnj!30dln,cyruswashington@bluesquarehub.com,cyrus,washington,,,fr,dhis2_id_6,,,,"manager, area_manager",, diff --git a/iaso/tests/fixtures/test_user_bulk_missing_columns.csv b/iaso/tests/fixtures/test_user_bulk_missing_columns.csv index 5060cb5245..de86bfad70 100644 --- a/iaso/tests/fixtures/test_user_bulk_missing_columns.csv +++ b/iaso/tests/fixtures/test_user_bulk_missing_columns.csv @@ -1,2 +1,2 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,user_roles,projects,phone_number -johndoeee,john,doe,johndoe@caramail.com,john,doe,,,fr,,,, +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,profile_language,dhis2_id,organization,user_roles,projects,phone_number,editable_org_unit_types +johndoeee,john,doe,johndoe@caramail.com,john,doe,,,fr,,,,, diff --git a/iaso/tests/test_create_users_from_csv.py b/iaso/tests/test_create_users_from_csv.py index c371a7e517..9fa4a348de 100644 --- a/iaso/tests/test_create_users_from_csv.py +++ b/iaso/tests/test_create_users_from_csv.py @@ -36,6 +36,7 @@ class BulkCreateCsvTestCase(APITestCase): "projects", "phone_number", "organization", + "editable_org_unit_types", ] @classmethod @@ -55,6 +56,11 @@ def setUpTestData(cls): cls.obi = cls.create_user_with_profile(username="obi", account=account1) cls.john = cls.create_user_with_profile(username="johndoe", account=account1, is_superuser=True) + cls.org_unit_type_region = m.OrgUnitType.objects.create(name="Region") + cls.org_unit_type_region.projects.add(cls.project) + cls.org_unit_type_country = m.OrgUnitType.objects.create(name="Country") + cls.org_unit_type_country.projects.add(cls.project) + cls.org_unit_parent = m.OrgUnit.objects.create( name="Parent org unit", id=1111, version=version1, source_ref="foo" ) @@ -90,7 +96,7 @@ def setUpTestData(cls): def test_upload_valid_csv(self): self.client.force_authenticate(self.yoda) self.source.projects.set([self.project]) - with self.assertNumQueries(81): + with self.assertNumQueries(82): with open("iaso/tests/fixtures/test_user_bulk_create_valid.csv") as csv_users: response = self.client.post(f"{BASE_URL}", {"file": csv_users}) @@ -539,6 +545,7 @@ def test_should_create_user_with_the_correct_org_unit_from_source_ref(self): "projects": "", "phone_number": "", "organization": "", + "editable_org_unit_types": "", } ) csv_bytes = csv_str.getvalue().encode() @@ -553,21 +560,18 @@ def test_should_create_user_with_the_correct_org_unit_from_source_ref(self): self.assertEqual(new_user.iaso_profile.org_units.first(), org_unit_a) self.assertEqual(org_unit_a.version_id, self.account1.default_version_id) - def test_should_create_user_with_the_correct_org_unit(self): + def test_bulk_create_should_fail_with_restricted_editable_org_unit_types(self): self.source.projects.set([self.project]) org_unit = self.org_unit_child - user = self.yoda - - org_unit_type_region = m.OrgUnitType.objects.create(name="Region") - org_unit_type_country = m.OrgUnitType.objects.create(name="Country") - - org_unit.org_unit_type = org_unit_type_country + org_unit.org_unit_type = self.org_unit_type_country org_unit.save() + user = self.yoda + user.iaso_profile.org_units.add(org_unit) user.iaso_profile.editable_org_unit_types.set( # Only org units of this type is now writable. - [org_unit_type_region] + [self.org_unit_type_region] ) user.user_permissions.add(Permission.objects.get(codename=permission._USERS_MANAGED)) @@ -596,6 +600,7 @@ def test_should_create_user_with_the_correct_org_unit(self): "projects": "", "phone_number": "", "organization": "", + "editable_org_unit_types": "", } ) csv_bytes = csv_str.getvalue().encode() @@ -608,6 +613,71 @@ def test_should_create_user_with_the_correct_org_unit(self): {"error": "Operation aborted. You don't have rights on the following org unit types: Country"}, ) + def test_bulk_create_user_with_editable_org_unit_types(self): + self.source.projects.set([self.project]) + org_unit = self.org_unit_child + org_unit.org_unit_type = self.org_unit_type_country + org_unit.save() + + non_admin_user = self.obi + non_admin_user.iaso_profile.org_units.add(org_unit) + non_admin_user.iaso_profile.editable_org_unit_types.set( + # Only org units of this type is now writable. + [self.org_unit_type_region] + ) + non_admin_user.user_permissions.add(Permission.objects.get(codename=permission._USERS_MANAGED)) + non_admin_user.user_permissions.remove(Permission.objects.get(codename=permission._USERS_ADMIN)) + self.assertTrue(non_admin_user.has_perm(permission.USERS_MANAGED)) + self.assertFalse(non_admin_user.has_perm(permission.USERS_ADMIN)) + + csv_str = io.StringIO() + writer = csv.DictWriter(csv_str, fieldnames=self.CSV_HEADER) + writer.writeheader() + writer.writerow( + { + "username": "john", + "password": "yodnj!30dln", + "email": "john@foo.com", + "first_name": "John", + "last_name": "Doe", + "orgunit": f"{org_unit.id}", + "orgunit__source_ref": "", + "profile_language": "fr", + "dhis2_id": "", + "organization": "", + "permissions": "", + "user_roles": "", + "projects": "", + "phone_number": "", + "editable_org_unit_types": f"{self.org_unit_type_region.pk},{self.org_unit_type_country.pk}", + } + ) + + csv_bytes = csv_str.getvalue().encode() + csv_file = SimpleUploadedFile("users.csv", csv_bytes) + self.client.force_authenticate(non_admin_user) + response = self.client.post(f"{BASE_URL}", {"file": csv_file}) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data, + {"error": "Operation aborted. You don't have rights on the following org unit types: Country"}, + ) + + admin_user = self.yoda + admin_user.iaso_profile.org_units.add(org_unit) + admin_user.user_permissions.add(Permission.objects.get(codename=permission._USERS_ADMIN)) + self.assertTrue(admin_user.has_perm(permission.USERS_ADMIN)) + + csv_bytes = csv_str.getvalue().encode() + csv_file = SimpleUploadedFile("users.csv", csv_bytes) + self.client.force_authenticate(admin_user) + response = self.client.post(f"{BASE_URL}", {"file": csv_file}) + self.assertEqual(response.status_code, 200) + new_user = User.objects.get(email="john@foo.com") + self.assertEqual(new_user.iaso_profile.editable_org_unit_types.count(), 2) + self.assertIn(self.org_unit_type_region, new_user.iaso_profile.editable_org_unit_types.all()) + self.assertIn(self.org_unit_type_country, new_user.iaso_profile.editable_org_unit_types.all()) + def test_valid_phone_number(self): phone_number = "+12345678912" expected_output = "+12345678912" From 4fbdf31a9eb6755bb22c3655c3a9106c37cd58ab Mon Sep 17 00:00:00 2001 From: Quang Son Le Date: Thu, 7 Nov 2024 11:31:47 +0100 Subject: [PATCH 04/46] IA-3660: convert org unit id to int in validation --- iaso/api/profiles/profiles.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iaso/api/profiles/profiles.py b/iaso/api/profiles/profiles.py index 710cb12a23..554ae712dd 100644 --- a/iaso/api/profiles/profiles.py +++ b/iaso/api/profiles/profiles.py @@ -421,6 +421,8 @@ def create(self, request): projects = self.validate_projects(request, profile) editable_org_unit_types = self.validate_editable_org_unit_types(request) except ProfileError as error: + # Delete profile if error since we're creating a new user + profile.delete() return JsonResponse( {"errorKey": error.field, "errorMessage": error.detail}, status=status.HTTP_400_BAD_REQUEST, @@ -657,7 +659,7 @@ def validate_org_units(self, request, profile) -> QuerySet[OrgUnit]: ) for org_unit in org_units: - org_unit_id = org_unit.get("id") + org_unit_id = int(org_unit.get("id")) if ( managed_org_units and org_unit_id not in managed_org_units From a34b5beef7f5d9c184105df0c00597e703d5fe60 Mon Sep 17 00:00:00 2001 From: Math VDH Date: Thu, 7 Nov 2024 11:32:46 +0000 Subject: [PATCH 05/46] POLIO-1689 Initial commit --- plugins/polio/api/dashboards/supply_chain.py | 10 +-- .../polio/api/vaccines/stock_management.py | 20 ++++++ .../0201_outgoingstockmovement_round.py | 20 ++++++ .../migrations/0202_populate_forma_round.py | 69 +++++++++++++++++++ plugins/polio/models/base.py | 1 + 5 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 plugins/polio/migrations/0201_outgoingstockmovement_round.py create mode 100644 plugins/polio/migrations/0202_populate_forma_round.py diff --git a/plugins/polio/api/dashboards/supply_chain.py b/plugins/polio/api/dashboards/supply_chain.py index 9203b86641..da35629003 100644 --- a/plugins/polio/api/dashboards/supply_chain.py +++ b/plugins/polio/api/dashboards/supply_chain.py @@ -61,10 +61,12 @@ def get_stock_in_hand(self, obj, vaccine_stock): return self.context["stock_in_hand_cache"][cache_key] def get_form_a_reception_date(self, obj, vaccine_stock): + query_filters = {"vaccine_stock": vaccine_stock, "campaign": obj.campaign} + if obj.rounds.exists(): + query_filters["round"] = obj.rounds.first() + latest_outgoing_stock_movement = ( - OutgoingStockMovement.objects.filter(vaccine_stock=vaccine_stock, campaign=obj.campaign) - .order_by("-form_a_reception_date") - .first() + OutgoingStockMovement.objects.filter(**query_filters).order_by("-form_a_reception_date").first() ) return latest_outgoing_stock_movement.form_a_reception_date if latest_outgoing_stock_movement else None @@ -146,7 +148,7 @@ class VaccineRequestFormDashboardViewSet(ModelViewSet): model = VaccineRequestForm serializer_class = VaccineRequestFormDashboardSerializer - @method_decorator(cache_page(60 * 60)) # Cache for 1 hour + # @method_decorator(cache_page(60 * 60)) # Cache for 1 hour def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) diff --git a/plugins/polio/api/vaccines/stock_management.py b/plugins/polio/api/vaccines/stock_management.py index ee0093756a..61f968cc18 100644 --- a/plugins/polio/api/vaccines/stock_management.py +++ b/plugins/polio/api/vaccines/stock_management.py @@ -22,6 +22,7 @@ VaccineRequestForm, VaccineStock, ) +from plugins.polio.models.base import Round vaccine_stock_id_param = openapi.Parameter( name="vaccine_stock", @@ -452,6 +453,7 @@ class Meta: "missing_vials", "document", "comment", + "round", ] def extract_campaign_data(self, validated_data): @@ -464,16 +466,34 @@ def extract_campaign_data(self, validated_data): return campaign return None + def extract_round_data(self, validated_data): + round_data = validated_data.pop("round", None) + campaign_data = validated_data.pop("campaign", None) + if round_data and campaign_data: + round = Round.objects.get( + number=round_data.get("number"), + campaign__obr_name=campaign_data.get("obr_name"), + account=self.context["request"].user.iaso_profile.account, + ) + return round + return None + def create(self, validated_data): campaign = self.extract_campaign_data(validated_data) + round = self.extract_round_data(validated_data) if campaign: validated_data["campaign"] = campaign + if round: + validated_data["round"] = round return OutgoingStockMovement.objects.create(**validated_data) def update(self, instance, validated_data): campaign = self.extract_campaign_data(validated_data) + round = self.extract_round_data(validated_data) if campaign: instance.campaign = campaign + if round: + instance.round = round return super().update(instance, validated_data) diff --git a/plugins/polio/migrations/0201_outgoingstockmovement_round.py b/plugins/polio/migrations/0201_outgoingstockmovement_round.py new file mode 100644 index 0000000000..0e0a9087c7 --- /dev/null +++ b/plugins/polio/migrations/0201_outgoingstockmovement_round.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.11 on 2024-11-07 10:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("polio", "0200_merge_20241025_0816"), + ] + + operations = [ + migrations.AddField( + model_name="outgoingstockmovement", + name="round", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="polio.round" + ), + ), + ] diff --git a/plugins/polio/migrations/0202_populate_forma_round.py b/plugins/polio/migrations/0202_populate_forma_round.py new file mode 100644 index 0000000000..3d53dca483 --- /dev/null +++ b/plugins/polio/migrations/0202_populate_forma_round.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.11 on 2024-11-07 10:11 + +from django.db import migrations + + +def populate_forma_round(apps, schema_editor): + OutgoingStockMovement = apps.get_model("polio", "OutgoingStockMovement") + Round = apps.get_model("polio", "Round") + + total_movements = OutgoingStockMovement.objects.count() + movements_with_round = 0 + print(f"\nTotal Form A movements to process: {total_movements}") + + for movement in OutgoingStockMovement.objects.all(): + # Get all rounds for this campaign ordered by start date + campaign_rounds = Round.objects.filter(campaign=movement.campaign).order_by("number") + print(f"\nProcessing Form A for campaign {movement.campaign.obr_name} with {campaign_rounds.count()} rounds") + for round in campaign_rounds: + print(f"Round {round.number} started at {round.started_at}") + + print(f"Form A reception date: {movement.form_a_reception_date}") + + # Find the appropriate round for this Form A + current_round = None + for i, round in enumerate(campaign_rounds): + next_round = campaign_rounds[i + 1] if i + 1 < len(campaign_rounds) else None + + if next_round: + # If Form A is between this round and next round + print(f"Checking if Form A is between {round.number} and {next_round.number}") + if ( + round.started_at + and next_round.started_at + and round.started_at <= movement.form_a_reception_date < next_round.started_at + ): + print(f"Form A is between {round.number} and {next_round.number}") + current_round = round + break + else: + # For the last round, just check if Form A is after its start + print(f"Checking if Form A is after {round.number}") + if round.started_at and round.started_at <= movement.form_a_reception_date: + print(f"Form A is after {round.number}") + current_round = round + break + + movement.round = current_round + movement.save() + + if current_round: + movements_with_round += 1 + print(f"Added round {current_round} to Form A") + else: + print("No matching round found for Form A") + + print(f"\nMigration complete: {movements_with_round}/{total_movements} Form A movements were assigned a round") + + +def reverse_populate_forma_round(apps, schema_editor): + OutgoingStockMovement = apps.get_model("polio", "OutgoingStockMovement") + OutgoingStockMovement.objects.all().update(round=None) + + +class Migration(migrations.Migration): + dependencies = [ + ("polio", "0201_outgoingstockmovement_round"), + ] + + operations = [migrations.RunPython(populate_forma_round, reverse_populate_forma_round)] diff --git a/plugins/polio/models/base.py b/plugins/polio/models/base.py index c3492c7e19..5efe81736c 100644 --- a/plugins/polio/models/base.py +++ b/plugins/polio/models/base.py @@ -1245,6 +1245,7 @@ def __str__(self): # Form A class OutgoingStockMovement(models.Model): campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE) + round = models.ForeignKey(Round, on_delete=models.CASCADE, null=True, blank=True) vaccine_stock = models.ForeignKey( VaccineStock, on_delete=models.CASCADE ) # Country can be deduced from the campaign From 1a311f4c9ab8e93432a8c4f34102608dbefb783b Mon Sep 17 00:00:00 2001 From: Quang Son Le Date: Thu, 7 Nov 2024 13:41:56 +0100 Subject: [PATCH 06/46] POLIO-1689: add round field to forma modal --- .../StockVariation/Modals/CreateEditFormA.tsx | 51 ++++-- .../StockManagement/hooks/api.ts | 147 +++++++++++------- .../VaccineModule/StockManagement/messages.ts | 4 + .../VaccineRequestForm/VaccineRequestForm.tsx | 42 +++-- 4 files changed, 152 insertions(+), 92 deletions(-) diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx index 72da4fb602..906b790073 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx @@ -20,7 +20,11 @@ import { import { useCampaignOptions, useSaveFormA } from '../../hooks/api'; import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; import { useFormAValidation } from './validation'; -import { acceptPDF, processErrorDocsBase } from '../../../SupplyChain/Details/utils'; +import { + acceptPDF, + processErrorDocsBase, +} from '../../../SupplyChain/Details/utils'; +import { useSkipEffectUntilValue } from '../../../SupplyChain/hooks/utils'; type Props = { formA?: any; @@ -47,6 +51,7 @@ export const CreateEditFormA: FunctionComponent = ({ initialValues: { id: formA?.id, campaign: formA?.campaign, + round: formA?.rounds, // lot_numbers: formA?.lot_numbers ?? '', report_date: formA?.report_date, form_a_reception_date: formA?.form_a_reception_date, @@ -54,22 +59,33 @@ export const CreateEditFormA: FunctionComponent = ({ // unusable_vials: formA?.unusable_vials, missing_vials: formA?.missing_vials, vaccine_stock: vaccineStockId, - document:formA?.document, + document: formA?.document, comment: formA?.comment ?? null, }, onSubmit: values => save(values), validationSchema, }); - const processDocumentErrors = useCallback(processErrorDocsBase, [formik.errors]); + const { setFieldValue } = formik; + const processDocumentErrors = useCallback(processErrorDocsBase, [ + formik.errors, + ]); - const { data: campaignOptions, isFetching: isFetchingCampaigns } = - useCampaignOptions(countryName, formik.values.campaign); + const { campaignOptions, isFetching, roundOptions } = useCampaignOptions( + countryName, + formik.values.campaign, + ); const titleMessage = formA?.id ? MESSAGES.edit : MESSAGES.create; const title = `${countryName} - ${vaccine}: ${formatMessage( titleMessage, )} ${formatMessage(MESSAGES.formA)}`; const allowConfirm = formik.isValid && !isEqual(formik.touched, {}); + const resetOnCampaignChange = useCallback(() => { + setFieldValue('round', undefined); + }, [setFieldValue]); + + useSkipEffectUntilValue(formik.values.campaign, resetOnCampaignChange); + return ( = ({ required options={campaignOptions} withMarginTop - isLoading={isFetchingCampaigns} + isLoading={isFetching} disabled={!countryName} /> + + + = ({ { if (files.length) { formik.setFieldTouched(`document`, true); @@ -148,10 +180,7 @@ export const CreateEditFormA: FunctionComponent = ({ }} multi={false} errors={processDocumentErrors(formik.errors.document)} - - placeholder={formatMessage( - MESSAGES.document, - )} + placeholder={formatMessage(MESSAGES.document)} /> diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/hooks/api.ts b/plugins/polio/js/src/domains/VaccineModule/StockManagement/hooks/api.ts index 9fef7a6b06..80ebae76a1 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/hooks/api.ts +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/hooks/api.ts @@ -1,4 +1,4 @@ -import { UrlParams } from 'bluesquare-components'; +import { UrlParams, useSafeIntl } from 'bluesquare-components'; import { useMemo } from 'react'; import { UseMutationResult, UseQueryResult } from 'react-query'; import { @@ -9,6 +9,7 @@ import { useUrlParams } from '../../../../../../../../hat/assets/js/apps/Iaso/ho import { deleteRequest, getRequest, + postRequest, } from '../../../../../../../../hat/assets/js/apps/Iaso/libs/Api'; import { useSnackMutation, @@ -27,6 +28,7 @@ import { useGetCampaigns, } from '../../../Campaigns/hooks/api/useGetCampaigns'; import { patchRequest2, postRequest2 } from '../../SupplyChain/hooks/api/vrf'; +import MESSAGES from '../messages'; const defaults = { order: 'country', @@ -251,47 +253,77 @@ export const useGetIncidentList = ( }; type UseCampaignOptionsResult = { - data: DropdownOptions[]; + roundOptions: DropdownOptions[]; + campaignOptions: DropdownOptions[]; isFetching: boolean; }; -// TODO get list of campaigns filtered by active vacccine +// TODO get list of campaigns filtered by active vaccine export const useCampaignOptions = ( countryName: string, campaignName?: string, ): UseCampaignOptionsResult => { + const { formatMessage } = useSafeIntl(); const queryOptions = { select: data => { if (!data) return []; - return data - .filter(c => c.top_level_org_unit_name === countryName) - .map(c => { - return { - label: c.obr_name, - value: c.obr_name, - }; - }); + return data.filter(c => c.top_level_org_unit_name === countryName); }, + // select: data => { + // if (!data) return []; + // return data + // .filter(c => c.top_level_org_unit_name === countryName) + // .map(c => { + // return { + // label: c.obr_name, + // value: c.obr_name, + // }; + // }); + // }, keepPreviousData: true, staleTime: 1000 * 60 * 15, // in MS cacheTime: 1000 * 60 * 5, }; - const { data: campaignsList, isFetching } = useGetCampaigns( + const { data, isFetching } = useGetCampaigns( {}, CAMPAIGNS_ENDPOINT, undefined, queryOptions, ); - const defaultList = useMemo( - () => [{ label: campaignName, value: campaignName }], - [campaignName], - ); - if ((campaignsList ?? []).length > 0) { - return { data: campaignsList, isFetching }; - } - if ((campaignsList ?? []).length === 0 && campaignName) { - return { data: defaultList as DropdownOptions[], isFetching }; - } - return { data: [], isFetching }; + + const roundOptions = useMemo(() => { + const selectedCampaign = (data ?? []).find( + campaign => campaign.obr_name === campaignName, + ); + return selectedCampaign + ? selectedCampaign.rounds.map(round => { + return { + label: `${formatMessage(MESSAGES.round)} ${round.number}`, + value: round.id, + }; + }) + : []; + }, [campaignName, data, formatMessage]); + + const campaignOptions = useMemo(() => { + const campaignsList = (data ?? []).map(c => { + return { + label: c.obr_name, + value: c.obr_name, + }; + }); + const defaultList = [{ label: campaignName, value: campaignName }]; + if ((campaignsList ?? []).length > 0) { + return campaignsList; + } + if ((campaignsList ?? []).length === 0 && campaignName) { + return defaultList; + } + return []; + }, [campaignName, data]); + + return useMemo(() => { + return { isFetching, campaignOptions, roundOptions }; + }, [campaignOptions, isFetching, roundOptions]); }; const createEditFormA = async (body: any) => { @@ -304,13 +336,15 @@ const createEditFormA = async (body: any) => { const filteredParams = copy ? Object.fromEntries( - Object.entries(copy).filter( - ([key, value]) => value !== undefined && value !== null && key !== 'document', - ), - ) + Object.entries(copy).filter( + ([key, value]) => + value !== undefined && + value !== null && + key !== 'document', + ), + ) : {}; - const requestBody: any = { url: `${modalUrl}outgoing_stock_movement/`, data: filteredParams, @@ -322,14 +356,11 @@ const createEditFormA = async (body: any) => { const fileData = { files: copy.document }; requestBody.data = data; requestBody.fileData = fileData; - } + } - if (body.id) { - requestBody['url'] = `${modalUrl}outgoing_stock_movement/${body.id}/` - return patchRequest2( - requestBody - ); + requestBody.url = `${modalUrl}outgoing_stock_movement/${body.id}/`; + return patchRequest2(requestBody); } return postRequest2(requestBody); }; @@ -343,7 +374,7 @@ export const useSaveFormA = () => { 'usable-vials', 'stock-management-summary', 'unusable-vials', - 'document' + 'document', ], }); }; @@ -357,13 +388,15 @@ const createEditDestruction = async (body: any) => { const filteredParams = copy ? Object.fromEntries( - Object.entries(copy).filter( - ([key, value]) => value !== undefined && value !== null && key !== 'document', - ), - ) + Object.entries(copy).filter( + ([key, value]) => + value !== undefined && + value !== null && + key !== 'document', + ), + ) : {}; - const requestBody: any = { url: `${modalUrl}destruction_report/`, data: filteredParams, @@ -375,14 +408,11 @@ const createEditDestruction = async (body: any) => { const fileData = { files: copy.document }; requestBody.data = data; requestBody.fileData = fileData; - } + } - if (body.id) { - requestBody['url'] = `${modalUrl}destruction_report/${body.id}/` - return patchRequest2( - requestBody - ); + requestBody.url = `${modalUrl}destruction_report/${body.id}/`; + return patchRequest2(requestBody); } return postRequest2(requestBody); }; @@ -396,7 +426,7 @@ export const useSaveDestruction = () => { 'usable-vials', 'stock-management-summary', 'unusable-vials', - 'document' + 'document', ], }); }; @@ -410,13 +440,15 @@ const createEditIncident = async (body: any) => { const filteredParams = copy ? Object.fromEntries( - Object.entries(copy).filter( - ([key, value]) => value !== undefined && value !== null && key !== 'document', - ), - ) + Object.entries(copy).filter( + ([key, value]) => + value !== undefined && + value !== null && + key !== 'document', + ), + ) : {}; - const requestBody: any = { url: `${modalUrl}incident_report/`, data: filteredParams, @@ -428,14 +460,11 @@ const createEditIncident = async (body: any) => { const fileData = { files: copy.document }; requestBody.data = data; requestBody.fileData = fileData; - } + } - if (body.id) { - requestBody['url'] = `${modalUrl}incident_report/${body.id}/` - return patchRequest2( - requestBody - ); + requestBody.url = `${modalUrl}incident_report/${body.id}/`; + return patchRequest2(requestBody); } return postRequest2(requestBody); }; @@ -449,7 +478,7 @@ export const useSaveIncident = () => { 'usable-vials', 'stock-management-summary', 'unusable-vials', - 'document' + 'document', ], }); }; diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/messages.ts b/plugins/polio/js/src/domains/VaccineModule/StockManagement/messages.ts index fe2a005424..72b491923e 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/messages.ts +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/messages.ts @@ -348,6 +348,10 @@ const MESSAGES = defineMessages({ id: 'iaso.polio.label.document', defaultMessage: 'Document', }, + round: { + id: 'iaso.polio.label.round', + defaultMessage: 'Round', + }, }); export default MESSAGES; diff --git a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx index 44322469ef..2891366618 100644 --- a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx @@ -19,8 +19,6 @@ import { acceptPDF, processErrorDocsBase } from '../utils'; type Props = { className?: string; vrfData: any }; - - export const VaccineRequestForm: FunctionComponent = ({ className, vrfData, @@ -40,7 +38,8 @@ export const VaccineRequestForm: FunctionComponent = ({ }, ]; - const { values, setFieldTouched, setFieldValue, errors } = useFormikContext(); + const { values, setFieldTouched, setFieldValue, errors } = + useFormikContext(); const { campaigns, vaccines, @@ -72,8 +71,6 @@ export const VaccineRequestForm: FunctionComponent = ({ [setFieldTouched, setFieldValue, values?.vrf?.comment, vrfDataComment], ); - - const resetOnCountryChange = useCallback(() => { setFieldValue('vrf.campaign', undefined); setFieldValue('vrf.vaccine_type', undefined); @@ -93,10 +90,8 @@ export const VaccineRequestForm: FunctionComponent = ({ const isNormalType = values?.vrf?.vrf_type === 'Normal'; - const processDocumentErrors = useCallback(processErrorDocsBase, [errors]); - return ( @@ -314,21 +309,30 @@ export const VaccineRequestForm: FunctionComponent = ({ /> - + { if (files.length) { - setFieldTouched('vrf.document', true); - setFieldValue('vrf.document', files); + setFieldTouched( + 'vrf.document', + true, + ); + setFieldValue( + 'vrf.document', + files, + ); } - console.log("File selected :" + files.length) - console.dir(files) }} multi={false} - errors={processDocumentErrors(errors.document)} - + errors={processDocumentErrors( + errors.document, + )} placeholder={formatMessage( MESSAGES.document, )} @@ -336,12 +340,7 @@ export const VaccineRequestForm: FunctionComponent = ({ - + {/* With MUI 5, the spacing isn't taken into account if there's only one item so the is used to compensate and align the TextArea with the other fields @@ -357,7 +356,6 @@ export const VaccineRequestForm: FunctionComponent = ({ debounceTime={0} /> - {/* From df1e49e885baddabddcfabae750a2b86d8511e2f Mon Sep 17 00:00:00 2001 From: Math VDH Date: Thu, 7 Nov 2024 13:01:13 +0000 Subject: [PATCH 07/46] POLIO-1689 Fix for initial value of round in front --- .../StockManagement/StockVariation/Modals/CreateEditFormA.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx index 906b790073..ef1057297c 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx @@ -51,7 +51,7 @@ export const CreateEditFormA: FunctionComponent = ({ initialValues: { id: formA?.id, campaign: formA?.campaign, - round: formA?.rounds, + round: formA?.round, // lot_numbers: formA?.lot_numbers ?? '', report_date: formA?.report_date, form_a_reception_date: formA?.form_a_reception_date, From af015087b59bec8755ecfbdb289147978ef04ec2 Mon Sep 17 00:00:00 2001 From: Math VDH Date: Thu, 7 Nov 2024 13:06:14 +0000 Subject: [PATCH 08/46] POLIO-1689 Fix API to interact correctly with Front --- plugins/polio/api/vaccines/stock_management.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/plugins/polio/api/vaccines/stock_management.py b/plugins/polio/api/vaccines/stock_management.py index 61f968cc18..d2b8ee4598 100644 --- a/plugins/polio/api/vaccines/stock_management.py +++ b/plugins/polio/api/vaccines/stock_management.py @@ -466,34 +466,16 @@ def extract_campaign_data(self, validated_data): return campaign return None - def extract_round_data(self, validated_data): - round_data = validated_data.pop("round", None) - campaign_data = validated_data.pop("campaign", None) - if round_data and campaign_data: - round = Round.objects.get( - number=round_data.get("number"), - campaign__obr_name=campaign_data.get("obr_name"), - account=self.context["request"].user.iaso_profile.account, - ) - return round - return None - def create(self, validated_data): campaign = self.extract_campaign_data(validated_data) - round = self.extract_round_data(validated_data) if campaign: validated_data["campaign"] = campaign - if round: - validated_data["round"] = round return OutgoingStockMovement.objects.create(**validated_data) def update(self, instance, validated_data): campaign = self.extract_campaign_data(validated_data) - round = self.extract_round_data(validated_data) if campaign: instance.campaign = campaign - if round: - instance.round = round return super().update(instance, validated_data) From e468d413d65919f9a4435a41f83f43f80aa1fbe0 Mon Sep 17 00:00:00 2001 From: Math VDH Date: Thu, 7 Nov 2024 13:40:00 +0000 Subject: [PATCH 09/46] POLIO-1689 Cleanup as will be changed in next version --- plugins/polio/api/dashboards/supply_chain.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/polio/api/dashboards/supply_chain.py b/plugins/polio/api/dashboards/supply_chain.py index da35629003..41742ae0fa 100644 --- a/plugins/polio/api/dashboards/supply_chain.py +++ b/plugins/polio/api/dashboards/supply_chain.py @@ -61,12 +61,12 @@ def get_stock_in_hand(self, obj, vaccine_stock): return self.context["stock_in_hand_cache"][cache_key] def get_form_a_reception_date(self, obj, vaccine_stock): - query_filters = {"vaccine_stock": vaccine_stock, "campaign": obj.campaign} - if obj.rounds.exists(): - query_filters["round"] = obj.rounds.first() - + # TODO: Remove this once the dashboard is updated to use the new form A model + # It will get this info by joining on the FormA.round id latest_outgoing_stock_movement = ( - OutgoingStockMovement.objects.filter(**query_filters).order_by("-form_a_reception_date").first() + OutgoingStockMovement.objects.filter(vaccine_stock=vaccine_stock, campaign=obj.campaign) + .order_by("-form_a_reception_date") + .first() ) return latest_outgoing_stock_movement.form_a_reception_date if latest_outgoing_stock_movement else None @@ -148,7 +148,7 @@ class VaccineRequestFormDashboardViewSet(ModelViewSet): model = VaccineRequestForm serializer_class = VaccineRequestFormDashboardSerializer - # @method_decorator(cache_page(60 * 60)) # Cache for 1 hour + @method_decorator(cache_page(60 * 60)) # Cache for 1 hour def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) From 05ad5cff6a590b09993202d3a2e93847f382d69c Mon Sep 17 00:00:00 2001 From: Math VDH Date: Thu, 7 Nov 2024 13:41:03 +0000 Subject: [PATCH 10/46] POLIO-1689 Cleanup --- plugins/polio/api/vaccines/stock_management.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/polio/api/vaccines/stock_management.py b/plugins/polio/api/vaccines/stock_management.py index d2b8ee4598..d891067bb1 100644 --- a/plugins/polio/api/vaccines/stock_management.py +++ b/plugins/polio/api/vaccines/stock_management.py @@ -22,7 +22,6 @@ VaccineRequestForm, VaccineStock, ) -from plugins.polio.models.base import Round vaccine_stock_id_param = openapi.Parameter( name="vaccine_stock", From 0523c1c6518181c851cb8a5d015c178fdee888b3 Mon Sep 17 00:00:00 2001 From: Math VDH Date: Thu, 7 Nov 2024 13:49:48 +0000 Subject: [PATCH 11/46] POLIO-1689 Basic test --- plugins/polio/tests/test_vaccine_stock_management.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/polio/tests/test_vaccine_stock_management.py b/plugins/polio/tests/test_vaccine_stock_management.py index 520366f560..57efb57fea 100644 --- a/plugins/polio/tests/test_vaccine_stock_management.py +++ b/plugins/polio/tests/test_vaccine_stock_management.py @@ -579,6 +579,7 @@ def test_outgoing_stock_movement_list(self): "usable_vials_used", "lot_numbers", "missing_vials", + "round", } self.assertTrue(expected_keys.issubset(first_result.keys())) From 3039f66eba7cd91149f0fe5cae96bac6364de9d5 Mon Sep 17 00:00:00 2001 From: HAKIZIMANA Franck Date: Thu, 7 Nov 2024 16:07:23 +0200 Subject: [PATCH 12/46] display correct instances count for an orgUnit and its descendants --- .../orgUnits/hooks/requests/useGetOrgUnitDetail.ts | 9 ++++++++- iaso/api/org_units.py | 7 ++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetOrgUnitDetail.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetOrgUnitDetail.ts index c814834f9d..b74b658a12 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetOrgUnitDetail.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetOrgUnitDetail.ts @@ -6,13 +6,20 @@ import { useSnackQuery } from '../../../../libs/apiHooks'; import { OrgUnit } from '../../types/orgUnit'; import MESSAGES from '../../messages'; +import { makeUrlWithParams } from '../../../../libs/utils'; export const useGetOrgUnitDetail = ( id?: number, ): UseQueryResult => { + const params: Record = { + instances_count: true, + }; + + const url = makeUrlWithParams(`/api/orgunits/${id}/`, params); + return useSnackQuery({ queryKey: ['orgunitdetail', id], - queryFn: () => getRequest(`/api/orgunits/${id}/`), + queryFn: () => getRequest(url), snackErrorMsg: MESSAGES.fetchOrgUnitError, options: { enabled: Boolean(id), diff --git a/iaso/api/org_units.py b/iaso/api/org_units.py index 729596a5fe..a3aee84bd8 100644 --- a/iaso/api/org_units.py +++ b/iaso/api/org_units.py @@ -763,9 +763,14 @@ def create(self, _, request): def retrieve(self, request, pk=None): org_unit: OrgUnit = get_object_or_404( - self.get_queryset().prefetch_related("reference_instances").annotate(instances_count=Count("instance")), + self.get_queryset().prefetch_related("reference_instances"), pk=pk, ) + + if request.query_params.get("instances_count"): + instances_count = org_unit.descendants().aggregate(Count("instance"))["instance__count"] + org_unit.instances_count = instances_count + self.check_object_permissions(request, org_unit) res = org_unit.as_dict_with_parents(light=False, light_parents=False) res["geo_json"] = None From 9fb2fdf2efb4bac94ba7d734f5fd9c597119676c Mon Sep 17 00:00:00 2001 From: HAKIZIMANA Franck Date: Fri, 8 Nov 2024 09:51:05 +0200 Subject: [PATCH 13/46] test retrieve orgUnit with instances_count --- iaso/tests/api/test_orgunits.py | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/iaso/tests/api/test_orgunits.py b/iaso/tests/api/test_orgunits.py index d90ddd339a..80a6b0a2e6 100644 --- a/iaso/tests/api/test_orgunits.py +++ b/iaso/tests/api/test_orgunits.py @@ -517,6 +517,51 @@ def test_org_unit_retrieve_ok_2(self): self.assertValidOrgUnitData(response.json()) self.assertEqual(response.data["reference_instances"], []) + def test_org_unit_retrieve_with_instances_count(self): + self.client.force_authenticate(self.yoda) + + parent_org_unit = m.OrgUnit.objects.create( + org_unit_type=self.jedi_council, + version=self.sw_version_1, + name="Parent", + validation_status=m.OrgUnit.VALIDATION_VALID, + ) + + descendant_org_unit = m.OrgUnit.objects.create( + org_unit_type=self.jedi_council, + version=self.sw_version_1, + name="Descendant", + parent=parent_org_unit, + validation_status=m.OrgUnit.VALIDATION_VALID, + ) + + self.create_form_instance( + form=self.form_1, + period="202001", + org_unit=descendant_org_unit, + project=self.project, + json={"name": "a", "age": 18, "gender": "M"}, + ) + + self.create_form_instance( + form=self.form_1, + period="202001", + org_unit=parent_org_unit, + project=self.project, + json={"name": "b", "age": 19, "gender": "F"}, + ) + # Test the descendant instances count + response_descendant = self.client.get(f"/api/orgunits/{descendant_org_unit.id}/?instances_count=true/") + self.assertJSONResponse(response_descendant, 200) + descendant_instances_count = response_descendant.json()["instances_count"] + self.assertEquals(descendant_instances_count, 1) + + # Test the parent instances count + response_parent = self.client.get(f"/api/orgunits/{parent_org_unit.id}/?instances_count=true/") + self.assertJSONResponse(response_parent, 200) + parent_instances_count = response_parent.json()["instances_count"] + self.assertEquals(parent_instances_count, 2) + def test_can_retrieve_org_units_in_csv_format(self): self.client.force_authenticate(self.yoda) response = self.client.get( From 4c9aa66485e0fed31c72a4dddfaf6ab62ea53399 Mon Sep 17 00:00:00 2001 From: Christophe Gerard Date: Fri, 8 Nov 2024 09:28:06 +0100 Subject: [PATCH 14/46] working preview --- .../Iaso/components/files/pdf/PdfPreview.tsx | 104 ++ .../apps/Iaso/components/files/pdf/utils.ts | 0 hat/webpack.dev.js | 8 +- package-lock.json | 949 ++++++++++++------ package.json | 2 + .../Modals/CreateEditDestruction.tsx | 39 +- .../StockVariation/Modals/CreateEditFormA.tsx | 34 +- .../Modals/CreateEditIncident.tsx | 58 +- .../StockVariation/Table/columns.tsx | 4 + .../Details/PreAlerts/PreAlert.tsx | 24 +- .../VaccineRequestForm/VaccineRequestForm.tsx | 62 +- .../SupplyChain/Details/utils.ts | 11 +- 12 files changed, 877 insertions(+), 418 deletions(-) create mode 100644 hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview.tsx create mode 100644 hat/assets/js/apps/Iaso/components/files/pdf/utils.ts diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview.tsx b/hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview.tsx new file mode 100644 index 0000000000..1d726a77ad --- /dev/null +++ b/hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview.tsx @@ -0,0 +1,104 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + IconButton, +} from '@mui/material'; +import { useSafeIntl } from 'bluesquare-components'; +import React, { useCallback, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { Document, Page, pdfjs } from 'react-pdf'; +import PdfSvgComponent from '../../svg/PdfSvgComponent'; + +// Set the workerSrc for pdfjs to enable the use of Web Workers. +// Web Workers allow the PDF.js library to process PDF files in a separate thread, +// keeping the main thread responsive and ensuring smooth UI interactions. +// Note: The PDF file itself is not transferred to the worker; only the processing is offloaded. +// This is necessary for the react-pdf library to function correctly. +if (!pdfjs.GlobalWorkerOptions.workerSrc) { + pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; +} + +type PdfPreviewProps = { + pdfUrl?: string; +}; + +export const MESSAGES = defineMessages({ + close: { + defaultMessage: 'Close', + id: 'blsq.buttons.label.close', + }, + download: { + defaultMessage: 'Download', + id: 'blsq.buttons.label.download', + }, +}); + +export const PdfPreview: React.FC = ({ pdfUrl }) => { + const [open, setOpen] = useState(false); // State to manage dialog open/close + + const { formatMessage } = useSafeIntl(); + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleDownload = useCallback(() => { + if (pdfUrl) { + const link = document.createElement('a'); + link.href = pdfUrl; + const urlParts = pdfUrl.split('/'); + const fileName = urlParts[urlParts.length - 1] || 'document.pdf'; + link.download = fileName; + link.click(); + } + }, [pdfUrl]); + return ( + <> + + + + {open && ( + + + + + + + + + + + + )} + + ); +}; diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/utils.ts b/hat/assets/js/apps/Iaso/components/files/pdf/utils.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hat/webpack.dev.js b/hat/webpack.dev.js index a1c60da7ee..15da577d6c 100644 --- a/hat/webpack.dev.js +++ b/hat/webpack.dev.js @@ -264,6 +264,11 @@ module.exports = { filename: 'videos/[name].[hash][ext]', }, }, + { + test: /\.mjs$/, + type: 'javascript/auto', + use: 'babel-loader', // or any other loader you are using + }, ], noParse: [require.resolve('typescript/lib/typescript.js')], // remove warning: https://github.com/microsoft/TypeScript/issues/39436 }, @@ -271,6 +276,7 @@ module.exports = { resolve: { alias: { + 'react/jsx-runtime': 'react/jsx-runtime.js', // see LIVE_COMPONENTS feature in doc ...(process.env.LIVE_COMPONENTS === 'true' && { 'bluesquare-components': path.resolve( @@ -292,7 +298,7 @@ module.exports = { : /* assets/js/apps path allow using absolute import eg: from 'iaso/libs/Api' */ ['node_modules', path.resolve(__dirname, 'assets/js/apps/')], - extensions: ['.js', '.tsx', '.ts'], + extensions: ['.js', '.jsx', '.ts', '.tsx'], }, stats: { errorDetails: true, diff --git a/package-lock.json b/package-lock.json index 2cda143192..3dffbf3f86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "node-fetch": "^2.6.7", "notistack": "^3.0.1", "npm": "10.5.2", + "pdfjs-dist": "^4.8.69", "polyfill-object.fromentries": "^1.0.1", "prop-types": "^15.7.2", "quill": "^2.0.2", @@ -54,6 +55,7 @@ "react-intl": "^5.20.7", "react-leaflet": "^3.2.5", "react-leaflet-markercluster": "^3.0.0-rc1", + "react-pdf": "^9.1.1", "react-query": "^3.18.1", "react-quilljs": "^2.0.0", "react-router-dom": "^6.22.3", @@ -136,11 +138,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dependencies": { - "@babel/highlight": "^7.24.6", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -455,9 +458,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "engines": { "node": ">=6.9.0" } @@ -500,6 +503,7 @@ "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.24.6", "chalk": "^2.4.2", @@ -2412,9 +2416,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2729,12 +2733,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -2759,6 +2764,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@icons/material": { @@ -2787,9 +2793,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { "node": ">=12" @@ -3047,7 +3053,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "dev": true, + "devOptional": true, "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", @@ -3067,7 +3073,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, + "devOptional": true, "dependencies": { "semver": "^6.0.0" }, @@ -3082,16 +3088,16 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "devOptional": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "devOptional": true, "bin": { "semver": "bin/semver.js" }, @@ -3856,9 +3862,9 @@ } }, "node_modules/@prettier/eslint/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3868,9 +3874,9 @@ } }, "node_modules/@prettier/eslint/node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -3881,9 +3887,9 @@ } }, "node_modules/@prettier/eslint/node_modules/vue-eslint-parser": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", - "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -4331,9 +4337,9 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" }, "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "version": "8.56.12", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", + "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", "dev": true, "dependencies": { "@types/estree": "*", @@ -4652,9 +4658,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4774,9 +4780,9 @@ } }, "node_modules/@typescript-eslint/experimental-utils/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4897,9 +4903,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4935,9 +4941,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -5405,7 +5411,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "devOptional": true }, "node_modules/abortcontroller-polyfill": { "version": "1.7.5", @@ -5426,9 +5432,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "bin": { "acorn": "bin/acorn" }, @@ -5468,7 +5474,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, + "devOptional": true, "dependencies": { "debug": "4" }, @@ -5490,15 +5496,15 @@ } }, "node_modules/ajv": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", - "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -5535,9 +5541,9 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true, "engines": { "node": ">=6" @@ -5583,6 +5589,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -5619,7 +5626,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true + "devOptional": true }, "node_modules/arch": { "version": "2.2.0", @@ -5652,7 +5659,7 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", "deprecated": "This package is no longer supported.", - "dev": true, + "devOptional": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -6173,9 +6180,9 @@ } }, "node_modules/babel-loader/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, "engines": { "node": ">=12.20" @@ -6325,9 +6332,9 @@ } }, "node_modules/babel-plugin-formatjs/node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6443,7 +6450,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -6503,6 +6510,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -6744,7 +6762,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -6892,9 +6910,9 @@ } }, "node_modules/camelcase-keys/node_modules/type-fest": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.3.tgz", - "integrity": "sha512-Q08/0IrpvM+NMY9PA2rti9Jb+JejTddwmwmVQGskAlhtcrw1wsRzoR6ode6mR+OAabNa75w/dxedSUY2mlphaQ==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", "dev": true, "engines": { "node": ">=16" @@ -6926,7 +6944,7 @@ "version": "2.11.2", "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", @@ -6957,9 +6975,9 @@ } }, "node_modules/canvg/node_modules/core-js": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", - "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "hasInstallScript": true, "optional": true, "funding": { @@ -7001,6 +7019,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -7014,6 +7033,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -7120,7 +7140,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -7296,7 +7316,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, + "devOptional": true, "bin": { "color-support": "bin.js" } @@ -7424,7 +7444,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true + "devOptional": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -7509,9 +7529,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.4.tgz", + "integrity": "sha512-9KdyVPPtLHjPAD7tcuzSFs64UfHlLJt7U6qP4/bFVLyjLceyizj6s6jO6YBaV5d0G7g/9KnY/dOpLR4Rcg8YDg==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -7567,9 +7587,9 @@ } }, "node_modules/css-loader/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -7820,9 +7840,9 @@ } }, "node_modules/cypress/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -8009,31 +8029,6 @@ "node": ">=18" } }, - "node_modules/data-urls/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dev": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dev": true, - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -8091,9 +8086,9 @@ "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -8106,6 +8101,11 @@ } } }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -8130,7 +8130,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "dev": true, + "devOptional": true, "dependencies": { "mimic-response": "^2.0.0" }, @@ -8150,6 +8150,15 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8405,7 +8414,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true + "devOptional": true }, "node_modules/depd": { "version": "2.0.0", @@ -8420,7 +8429,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -8439,7 +8447,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -8631,9 +8639,9 @@ "integrity": "sha512-d0EFmtLPjctczO3LogReyM2pbBiiZbnsKnGF+cdZhsYzHm/A0GV7W94kqzLD8SN4O3f3iHlgLUChqghgyznvCQ==" }, "node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "node_modules/emojis-list": { @@ -8658,7 +8666,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, + "devOptional": true, "dependencies": { "once": "^1.4.0" } @@ -8967,16 +8975,17 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -9976,6 +9985,15 @@ "node": ">=4" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.21.1", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", @@ -10033,12 +10051,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "dev": true - }, "node_modules/express/node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -10146,6 +10158,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -10541,6 +10559,12 @@ } ] }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -10559,7 +10583,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, + "devOptional": true, "dependencies": { "minipass": "^3.0.0" }, @@ -10571,7 +10595,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10583,7 +10607,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/fs-monkey": { "version": "1.0.6", @@ -10656,7 +10680,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", "deprecated": "This package is no longer supported.", - "dev": true, + "devOptional": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", @@ -10781,6 +10805,12 @@ "assert-plus": "^1.0.0" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "optional": true + }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -10986,6 +11016,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { "node": ">=4" } @@ -11042,7 +11073,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true + "devOptional": true }, "node_modules/hasha": { "version": "5.2.2", @@ -11141,15 +11172,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/html-element-map": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz", @@ -11344,7 +11366,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, + "devOptional": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -11417,7 +11439,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -11434,9 +11456,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -11752,11 +11774,14 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11832,7 +11857,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -12324,9 +12349,9 @@ } }, "node_modules/istanbul-lib-report/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -12397,16 +12422,13 @@ } }, "node_modules/jackspeak": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", - "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -12530,9 +12552,9 @@ } }, "node_modules/jsdom/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dev": true, "dependencies": { "asynckit": "^0.4.0", @@ -12544,9 +12566,9 @@ } }, "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -12556,31 +12578,6 @@ "node": ">= 14" } }, - "node_modules/jsdom/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dev": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dev": true, - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -12710,9 +12707,9 @@ } }, "node_modules/jspdf/node_modules/core-js": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", - "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "hasInstallScript": true, "optional": true, "funding": { @@ -13419,6 +13416,14 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "node_modules/make-cancellable-promise": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", + "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", + "funding": { + "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -13450,6 +13455,14 @@ "semver": "bin/semver" } }, + "node_modules/make-event-props": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", + "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, "node_modules/make-plural": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.4.0.tgz", @@ -13528,6 +13541,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-refs": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz", + "integrity": "sha512-nqXPXbso+1dcKDpPCXvwZyJILz+vSLqGGOnDrYHQYE+B8n9JTCekVLC65AfCpR4ggVyA/45Y0iR9LDyS2iI+zA==", + "funding": { + "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -13612,7 +13641,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" }, @@ -13719,7 +13748,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "devOptional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -13728,7 +13757,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -13737,7 +13766,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, + "devOptional": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -13750,7 +13779,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -13762,13 +13791,13 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, + "devOptional": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -13776,6 +13805,12 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "optional": true + }, "node_modules/mocha": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", @@ -13811,15 +13846,6 @@ "node": ">= 14.0.0" } }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/mocha/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -13829,29 +13855,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/mocha/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -13873,12 +13876,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -13948,10 +13945,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -13985,7 +13983,7 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", - "dev": true + "devOptional": true }, "node_modules/nano-time": { "version": "1.0.0", @@ -14018,6 +14016,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -14105,6 +14109,36 @@ "node": ">= 10.13" } }, + "node_modules/node-abi": { + "version": "3.71.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", + "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "optional": true + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -14173,7 +14207,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dev": true, + "devOptional": true, "dependencies": { "abbrev": "1" }, @@ -16824,7 +16858,7 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", "deprecated": "This package is no longer supported.", - "dev": true, + "devOptional": true, "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", @@ -17459,6 +17493,12 @@ "node": ">=8" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/parchment": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", @@ -17580,13 +17620,16 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true }, "node_modules/path-type": { "version": "4.0.0", @@ -17596,6 +17639,15 @@ "node": ">=8" } }, + "node_modules/path2d": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.2.tgz", + "integrity": "sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -17605,6 +17657,33 @@ "node": "*" } }, + "node_modules/pdfjs-dist": { + "version": "4.8.69", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.8.69.tgz", + "integrity": "sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^3.0.0-rc2", + "path2d": "^0.2.1" + } + }, + "node_modules/pdfjs-dist/node_modules/canvas": { + "version": "3.0.0-rc2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.0.0-rc2.tgz", + "integrity": "sha512-esx4bYDznnqgRX4G8kaEaf0W3q8xIc51WpmrIitDzmcoEgwnv9wSKdzT6UxWZ4wkVu5+ileofppX0TpyviJRdQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "simple-get": "^3.0.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -17858,9 +17937,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -17876,6 +17955,84 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -17886,9 +18043,9 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -18036,9 +18193,9 @@ "dev": true }, "node_modules/prettier-eslint-cli/node_modules/core-js": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", - "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "dev": true, "hasInstallScript": true, "funding": { @@ -18047,9 +18204,9 @@ } }, "node_modules/prettier-eslint-cli/node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", @@ -18084,23 +18241,21 @@ } }, "node_modules/prettier-eslint-cli/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -18115,9 +18270,9 @@ } }, "node_modules/prettier-eslint-cli/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -18599,9 +18754,9 @@ } }, "node_modules/prettier-eslint/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -18783,7 +18938,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, + "devOptional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -18958,6 +19113,36 @@ "node": ">=0.10.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "optional": true + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", @@ -19102,6 +19287,46 @@ "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-pdf": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.1.1.tgz", + "integrity": "sha512-Cn3RTJZMqVOOCgLMRXDamLk4LPGfyB2Np3OwQAUjmHIh47EpuGW1OpAA1Z1GVDLoHx4d5duEDo/YbUkDbr4QFQ==", + "dependencies": { + "clsx": "^2.0.0", + "dequal": "^2.0.3", + "make-cancellable-promise": "^1.3.1", + "make-event-props": "^1.6.0", + "merge-refs": "^1.3.0", + "pdfjs-dist": "4.4.168", + "tiny-invariant": "^1.0.0", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-pdf/node_modules/pdfjs-dist": { + "version": "4.4.168", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz", + "integrity": "sha512-MbkAjpwka/dMHaCfQ75RY1FXX3IewBVu6NGZOcxerRFlaBiIkZmUoR0jotX5VUzYZEXAGzSFtknWs5xRKliXPA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d": "^0.2.0" + } + }, "node_modules/react-phone-input-material-ui": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/react-phone-input-material-ui/-/react-phone-input-material-ui-2.18.1.tgz", @@ -19301,7 +19526,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -19722,9 +19947,9 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, "node_modules/rrweb-cssom": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.0.tgz", - "integrity": "sha512-KlSv0pm9kgQSRxXEMgtivPJ4h826YHsuob8pSHcfSZsSXGtvpEAie8S0AnXuObEJ7nhikOb4ahwxDm0H2yW17g==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true }, "node_modules/rst-selector-parser": { @@ -19966,12 +20191,6 @@ "node": ">= 0.8" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -20078,7 +20297,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true + "devOptional": true }, "node_modules/set-function-length": { "version": "1.2.2", @@ -20181,13 +20400,13 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "devOptional": true }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -20207,7 +20426,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "dev": true, + "devOptional": true, "dependencies": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -20540,19 +20759,25 @@ } }, "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "devOptional": true, "dependencies": { - "safe-buffer": "~5.2.0" + "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "devOptional": true + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -20587,7 +20812,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "devOptional": true }, "node_modules/string.prototype.matchall": { "version": "4.0.11", @@ -20668,7 +20893,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -20702,7 +20927,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -20815,6 +21040,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -20943,7 +21169,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, + "devOptional": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -20956,11 +21182,45 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "optional": true + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/terser": { "version": "5.31.0", @@ -21222,6 +21482,18 @@ "node": ">= 4.0.0" } }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -21299,9 +21571,9 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/ts-loader/node_modules/enhanced-resolve": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", - "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -21319,9 +21591,9 @@ } }, "node_modules/ts-loader/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -21390,9 +21662,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -21419,7 +21691,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, + "devOptional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -21820,7 +22092,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "devOptional": true }, "node_modules/utils-merge": { "version": "1.0.1", @@ -22023,6 +22295,14 @@ "node": ">=18" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -22294,9 +22574,9 @@ } }, "node_modules/webpack/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/webpack/node_modules/ajv": { "version": "6.12.6", @@ -22415,6 +22695,19 @@ "node": ">=18" } }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -22519,7 +22812,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, + "devOptional": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } diff --git a/package.json b/package.json index 1ecdae35e6..bbc2d9f081 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "node-fetch": "^2.6.7", "notistack": "^3.0.1", "npm": "10.5.2", + "pdfjs-dist": "^4.8.69", "polyfill-object.fromentries": "^1.0.1", "prop-types": "^15.7.2", "quill": "^2.0.2", @@ -93,6 +94,7 @@ "react-intl": "^5.20.7", "react-leaflet": "^3.2.5", "react-leaflet-markercluster": "^3.0.0-rc1", + "react-pdf": "^9.1.1", "react-query": "^3.18.1", "react-quilljs": "^2.0.0", "react-router-dom": "^6.22.3", diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditDestruction.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditDestruction.tsx index 8517997f4b..18c696f270 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditDestruction.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditDestruction.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useCallback } from 'react'; +import { Box } from '@mui/material'; import { AddButton, ConfirmCancelModal, @@ -8,18 +8,21 @@ import { } from 'bluesquare-components'; import { Field, FormikProvider, useFormik } from 'formik'; import { isEqual } from 'lodash'; -import { Box } from '@mui/material'; -import { Vaccine } from '../../../../../constants/types'; -import MESSAGES from '../../messages'; +import React, { FunctionComponent, useCallback } from 'react'; +import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; import { - TextInput, DateInput, NumberInput, + TextInput, } from '../../../../../components/Inputs'; +import { Vaccine } from '../../../../../constants/types'; +import { + acceptPDF, + processErrorDocsBase, +} from '../../../SupplyChain/Details/utils'; import { useSaveDestruction } from '../../hooks/api'; -import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; +import MESSAGES from '../../messages'; import { useDestructionValidation } from './validation'; -import { acceptPDF, processErrorDocsBase } from '../../../SupplyChain/Details/utils'; type Props = { destruction?: any; @@ -52,14 +55,16 @@ export const CreateEditDestruction: FunctionComponent = ({ unusable_vials_destroyed: destruction?.unusable_vials_destroyed, // lot_numbers: destruction?.lot_numbers, vaccine_stock: vaccineStockId, - document:destruction?.document, - comment:destruction?.comment ?? null + document: destruction?.document, + comment: destruction?.comment ?? null, }, onSubmit: values => save(values), validationSchema, }); - const processDocumentErrors = useCallback(processErrorDocsBase, [formik.errors]); + const processDocumentErrors = useCallback(processErrorDocsBase, [ + formik.errors, + ]); const titleMessage = destruction?.id ? MESSAGES.edit : MESSAGES.create; const title = `${countryName} - ${vaccine}: ${formatMessage( @@ -135,7 +140,11 @@ export const CreateEditDestruction: FunctionComponent = ({ { if (files.length) { formik.setFieldTouched(`document`, true); @@ -144,10 +153,7 @@ export const CreateEditDestruction: FunctionComponent = ({ }} multi={false} errors={processDocumentErrors(formik.errors.document)} - - placeholder={formatMessage( - MESSAGES.document, - )} + placeholder={formatMessage(MESSAGES.document)} /> @@ -159,5 +165,6 @@ const modalWithIcon = makeFullModal(CreateEditDestruction, EditIconButton); export { modalWithButton as CreateDestruction, - modalWithIcon as EditDestruction, + modalWithIcon as EditDestruction }; + diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx index 72da4fb602..ae25b88fb8 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useCallback } from 'react'; +import { Box } from '@mui/material'; import { AddButton, ConfirmCancelModal, @@ -8,19 +8,22 @@ import { } from 'bluesquare-components'; import { Field, FormikProvider, useFormik } from 'formik'; import { isEqual } from 'lodash'; -import { Box } from '@mui/material'; -import { Vaccine } from '../../../../../constants/types'; -import MESSAGES from '../../messages'; -import { SingleSelect } from '../../../../../components/Inputs/SingleSelect'; +import React, { FunctionComponent, useCallback } from 'react'; +import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; import { DateInput, NumberInput, TextInput, } from '../../../../../components/Inputs'; +import { SingleSelect } from '../../../../../components/Inputs/SingleSelect'; +import { Vaccine } from '../../../../../constants/types'; +import { + acceptPDF, + processErrorDocsBase, +} from '../../../SupplyChain/Details/utils'; import { useCampaignOptions, useSaveFormA } from '../../hooks/api'; -import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; +import MESSAGES from '../../messages'; import { useFormAValidation } from './validation'; -import { acceptPDF, processErrorDocsBase } from '../../../SupplyChain/Details/utils'; type Props = { formA?: any; @@ -54,13 +57,15 @@ export const CreateEditFormA: FunctionComponent = ({ // unusable_vials: formA?.unusable_vials, missing_vials: formA?.missing_vials, vaccine_stock: vaccineStockId, - document:formA?.document, + document: formA?.document, comment: formA?.comment ?? null, }, onSubmit: values => save(values), validationSchema, }); - const processDocumentErrors = useCallback(processErrorDocsBase, [formik.errors]); + const processDocumentErrors = useCallback(processErrorDocsBase, [ + formik.errors, + ]); const { data: campaignOptions, isFetching: isFetchingCampaigns } = useCampaignOptions(countryName, formik.values.campaign); @@ -139,7 +144,11 @@ export const CreateEditFormA: FunctionComponent = ({ { if (files.length) { formik.setFieldTouched(`document`, true); @@ -148,10 +157,7 @@ export const CreateEditFormA: FunctionComponent = ({ }} multi={false} errors={processDocumentErrors(formik.errors.document)} - - placeholder={formatMessage( - MESSAGES.document, - )} + placeholder={formatMessage(MESSAGES.document)} /> diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditIncident.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditIncident.tsx index 42fdfb9fe2..04d46f92e5 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditIncident.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditIncident.tsx @@ -2,6 +2,7 @@ import { Box, FormControl, FormControlLabel, + Grid, Radio, RadioGroup, Typography, @@ -17,6 +18,7 @@ import { Field, FormikProvider, useFormik } from 'formik'; import { isEqual } from 'lodash'; import React, { FunctionComponent, useCallback, useMemo } from 'react'; import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; +import { PdfPreview } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview'; import { DateInput, NumberInput, @@ -24,12 +26,15 @@ import { } from '../../../../../components/Inputs'; import { SingleSelect } from '../../../../../components/Inputs/SingleSelect'; import { Vaccine } from '../../../../../constants/types'; +import { + acceptPDF, + processErrorDocsBase, +} from '../../../SupplyChain/Details/utils'; import { useSaveIncident } from '../../hooks/api'; import { useGetMovementDescription } from '../../hooks/useGetMovementDescription'; import MESSAGES from '../../messages'; import { useIncidentOptions } from './useIncidentOptions'; import { useIncidentValidation } from './validation'; -import { acceptPDF, processErrorDocsBase } from '../../../SupplyChain/Details/utils'; type Props = { incident?: any; @@ -222,13 +227,15 @@ export const CreateEditIncident: FunctionComponent = ({ unusable_vials: incident?.unusable_vials || 0, movement: getInitialMovement(incident), vaccine_stock: vaccineStockId, - document: incident?.document + document: incident?.document, }, onSubmit: handleSubmit, validationSchema, }); - const processDocumentErrors = useCallback(processErrorDocsBase, [formik.errors]); + const processDocumentErrors = useCallback(processErrorDocsBase, [ + formik.errors, + ]); const incidentTypeOptions = useIncidentOptions(); const titleMessage = incident?.id ? MESSAGES.edit : MESSAGES.create; const title = `${countryName} - ${vaccine}: ${formatMessage( @@ -398,22 +405,37 @@ export const CreateEditIncident: FunctionComponent = ({ /> - { - if (files.length) { - formik.setFieldTouched(`document`, true); - formik.setFieldValue(`document`, files); - } - }} - multi={false} - errors={processDocumentErrors(formik.errors.document)} - - placeholder={formatMessage( - MESSAGES.document, + + + { + if (files.length) { + formik.setFieldTouched( + `document`, + true, + ); + formik.setFieldValue(`document`, files); + } + }} + multi={false} + errors={processDocumentErrors( + formik.errors.document, + )} + placeholder={formatMessage(MESSAGES.document)} + /> + + {incident.document && ( + + + )} - /> + diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Table/columns.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Table/columns.tsx index 4c103a2c3b..0d540e9749 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Table/columns.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Table/columns.tsx @@ -4,6 +4,7 @@ import React, { useMemo } from 'react'; import { DateCell } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Cells/DateTimeCell'; import { NumberCell } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Cells/NumberCell'; import DeleteDialog from '../../../../../../../../../hat/assets/js/apps/Iaso/components/dialogs/DeleteDialogComponent'; +import { PdfPreview } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview'; import { userHasPermission } from '../../../../../../../../../hat/assets/js/apps/Iaso/domains/users/utils'; import { useCurrentUser } from '../../../../../../../../../hat/assets/js/apps/Iaso/utils/usersUtils'; import { STOCK_MANAGEMENT_WRITE } from '../../../../../constants/permissions'; @@ -269,6 +270,9 @@ export const useIncidentTableColumns = ( Cell: settings => { return ( <> + = ({ index, vaccine }) => { { if (files.length) { - setFieldTouched(`pre_alerts[${index}].document`, true); - setFieldValue(`pre_alerts[${index}].document`, files); + setFieldTouched( + `pre_alerts[${index}].document`, + true, + ); + setFieldValue( + `pre_alerts[${index}].document`, + files, + ); } }} multi={false} - errors={processDocumentErrors(errors[index]?.document)} - + errors={processDocumentErrors( + errors[index]?.document, + )} placeholder={formatMessage( MESSAGES.document, )} diff --git a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx index 44322469ef..886ef014fb 100644 --- a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx @@ -1,26 +1,24 @@ -import React, { FunctionComponent, useCallback, useEffect } from 'react'; import { Box, Grid, Typography } from '@mui/material'; -import { Field, useFormikContext } from 'formik'; import { FilesUpload, useSafeIntl } from 'bluesquare-components'; -import { SingleSelect } from '../../../../../components/Inputs/SingleSelect'; -import { MultiSelect } from '../../../../../components/Inputs/MultiSelect'; -import { DateInput } from '../../../../../components/Inputs/DateInput'; -import { NumberInput } from '../../../../../components/Inputs'; +import { Field, useFormikContext } from 'formik'; +import React, { FunctionComponent, useCallback, useEffect } from 'react'; import { TextArea } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/forms/TextArea'; -import MESSAGES from '../../messages'; +import { NumberInput } from '../../../../../components/Inputs'; +import { DateInput } from '../../../../../components/Inputs/DateInput'; +import { MultiSelect } from '../../../../../components/Inputs/MultiSelect'; +import { SingleSelect } from '../../../../../components/Inputs/SingleSelect'; import { renderRoundTag, useCampaignDropDowns, useGetCountriesOptions, } from '../../hooks/api/vrf'; -import { useSharedStyles } from '../shared'; import { useSkipEffectUntilValue } from '../../hooks/utils'; +import MESSAGES from '../../messages'; +import { useSharedStyles } from '../shared'; import { acceptPDF, processErrorDocsBase } from '../utils'; type Props = { className?: string; vrfData: any }; - - export const VaccineRequestForm: FunctionComponent = ({ className, vrfData, @@ -40,7 +38,8 @@ export const VaccineRequestForm: FunctionComponent = ({ }, ]; - const { values, setFieldTouched, setFieldValue, errors } = useFormikContext(); + const { values, setFieldTouched, setFieldValue, errors } = + useFormikContext(); const { campaigns, vaccines, @@ -72,8 +71,6 @@ export const VaccineRequestForm: FunctionComponent = ({ [setFieldTouched, setFieldValue, values?.vrf?.comment, vrfDataComment], ); - - const resetOnCountryChange = useCallback(() => { setFieldValue('vrf.campaign', undefined); setFieldValue('vrf.vaccine_type', undefined); @@ -93,10 +90,8 @@ export const VaccineRequestForm: FunctionComponent = ({ const isNormalType = values?.vrf?.vrf_type === 'Normal'; - const processDocumentErrors = useCallback(processErrorDocsBase, [errors]); - return ( @@ -314,21 +309,34 @@ export const VaccineRequestForm: FunctionComponent = ({ /> - + { if (files.length) { - setFieldTouched('vrf.document', true); - setFieldValue('vrf.document', files); + setFieldTouched( + 'vrf.document', + true, + ); + setFieldValue( + 'vrf.document', + files, + ); } - console.log("File selected :" + files.length) - console.dir(files) + console.log( + `File selected :${files.length}`, + ); + console.dir(files); }} multi={false} - errors={processDocumentErrors(errors.document)} - + errors={processDocumentErrors( + errors.document, + )} placeholder={formatMessage( MESSAGES.document, )} @@ -336,12 +344,7 @@ export const VaccineRequestForm: FunctionComponent = ({ - + {/* With MUI 5, the spacing isn't taken into account if there's only one item so the is used to compensate and align the TextArea with the other fields @@ -357,7 +360,6 @@ export const VaccineRequestForm: FunctionComponent = ({ debounceTime={0} /> - {/* diff --git a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/utils.ts b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/utils.ts index ae0a291e9b..940a765eb4 100644 --- a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/utils.ts +++ b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/utils.ts @@ -1,4 +1,5 @@ import { FormikErrors, FormikProps, FormikTouched } from 'formik'; +import { Accept } from 'react-dropzone'; import { baseUrls } from '../../../../constants/urls'; import { VRF } from '../constants'; import { @@ -8,7 +9,6 @@ import { UseHandleSubmitArgs, VAR, } from '../types'; -import { Accept } from 'react-dropzone'; type SaveAllArgs = { changedTabs: TabValue[]; @@ -201,13 +201,12 @@ export const makeHandleSubmit = ); }; - export const acceptPDF: Accept = { 'application/pdf': ['.pdf'], }; -export const processErrorDocsBase = (err_docs) => { +export const processErrorDocsBase = err_docs => { if (!err_docs) return []; - if (!Array.isArray(err_docs)) return [err_docs]; - else return err_docs; -}; \ No newline at end of file + if (!Array.isArray(err_docs)) return [err_docs]; + return err_docs; +}; From c6091f89938849d26fb250b49ccc53bb486a48b9 Mon Sep 17 00:00:00 2001 From: Bram Jans Date: Wed, 6 Nov 2024 09:40:18 +0100 Subject: [PATCH 15/46] IA-3625 Mobile setup zip: Add missing API calls Added files for the groupsets and deleted entities API calls. --- iaso/tasks/export_mobile_app_setup_for_user.py | 5 ++++- iaso/tasks/utils/mobile_app_setup_api_calls.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/iaso/tasks/export_mobile_app_setup_for_user.py b/iaso/tasks/export_mobile_app_setup_for_user.py index ebb85a9fdd..e25e498beb 100644 --- a/iaso/tasks/export_mobile_app_setup_for_user.py +++ b/iaso/tasks/export_mobile_app_setup_for_user.py @@ -73,8 +73,11 @@ def export_mobile_app_setup_for_user( the_task.report_progress_and_stop_if_killed(progress_value=2) for call in API_CALLS: + the_task.report_progress_and_stop_if_killed( + progress_value=the_task.progress_value + 1, + progress_message=f"Fetching {call['filename']}", + ) _get_resource(iaso_client, call, tmp_dir, project.app_id, feature_flags) - the_task.report_progress_and_stop_if_killed(progress_value=the_task.progress_value + 1) s3_object_name = _compress_and_upload_to_s3(tmp_dir, export_name, password) diff --git a/iaso/tasks/utils/mobile_app_setup_api_calls.py b/iaso/tasks/utils/mobile_app_setup_api_calls.py index 66418c999b..deb990268c 100644 --- a/iaso/tasks/utils/mobile_app_setup_api_calls.py +++ b/iaso/tasks/utils/mobile_app_setup_api_calls.py @@ -7,6 +7,10 @@ "path": "/api/mobile/groups/", "filename": "groups", }, + { + "path": "/api/mobile/group_sets/", + "filename": "groupsets", + }, { "path": "/api/mobile/forms/", "filename": "forms", @@ -58,6 +62,12 @@ "filename": "entities", "paginated": True, }, + { + "path": "/api/mobile/entities/deleted/", + "required_feature_flag": "ENTITY", + "filename": "deletedentities", + "paginated": True, + }, { "path": "/api/mobile/entitytypes/", "required_feature_flag": "ENTITY", From 7c01d939747377aae4440383403054875411387a Mon Sep 17 00:00:00 2001 From: Bram Jans Date: Fri, 8 Nov 2024 09:46:27 +0100 Subject: [PATCH 16/46] IA-3625 Add forgotten ?showDeleted=true to group and groupset calls --- iaso/tasks/utils/mobile_app_setup_api_calls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iaso/tasks/utils/mobile_app_setup_api_calls.py b/iaso/tasks/utils/mobile_app_setup_api_calls.py index deb990268c..a6b8fc4e97 100644 --- a/iaso/tasks/utils/mobile_app_setup_api_calls.py +++ b/iaso/tasks/utils/mobile_app_setup_api_calls.py @@ -4,11 +4,11 @@ "filename": "orgunittypes", }, { - "path": "/api/mobile/groups/", + "path": "/api/mobile/groups/?showDeleted=true", "filename": "groups", }, { - "path": "/api/mobile/group_sets/", + "path": "/api/mobile/group_sets/?showDeleted=true", "filename": "groupsets", }, { From a2d46125deea7a9ee05e371be142eb74c6f3fd4d Mon Sep 17 00:00:00 2001 From: Math VDH Date: Fri, 8 Nov 2024 09:38:22 +0000 Subject: [PATCH 17/46] POLIO-1721 Fix bug where arrival reports do not update even if it seems so --- .../SupplyChain/hooks/api/utils.tsx | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/hooks/api/utils.tsx b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/hooks/api/utils.tsx index a4cdd82858..f210a63365 100644 --- a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/hooks/api/utils.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/hooks/api/utils.tsx @@ -54,25 +54,34 @@ export const saveTab = ( const formData = new FormData(); data.forEach((item: any, index: number) => { - Object.keys(item).forEach(key => { - if (key === 'document' && item[key]) { - if (Array.isArray(item[key])) { - formData.append(`pre_alerts[${index}].${key}`, item[key][0]); - } else if (typeof item[key] === 'string') { - const filePath = item[key]; + Object.keys(item).forEach(keyy => { + if (keyy === 'document' && item[keyy]) { + if (Array.isArray(item[keyy])) { + formData.append( + `${key}[${index}].${keyy}`, + item[keyy][0], + ); + } else if (typeof item[keyy] === 'string') { + const filePath = item[keyy]; const fileName = filePath.split('/').pop(); - const file = new File([filePath], fileName || 'document'); - formData.append(`pre_alerts[${index}].${key}`, file); + const file = new File( + [filePath], + fileName || 'document', + ); + formData.append(`${key}[${index}].${keyy}`, file); } - - } else if (item[key] !== null && item[key] !== undefined) { - formData.append(`pre_alerts[${index}].${key}`, item[key]); + } else if (item[keyy] !== null && item[keyy] !== undefined) { + formData.append(`${key}[${index}].${keyy}`, item[keyy]); } }); }); - - const method = url.includes('update') ? 'PATCH' : url.includes('add') ? 'POST' : 'GET'; + // eslint-disable-next-line no-nested-ternary + const method = url.includes('update') + ? 'PATCH' + : url.includes('add') + ? 'POST' + : 'GET'; return fetch(url, { method, body: formData, From 6f8d7c959e8d12611a9baa1df707c228f684d5d5 Mon Sep 17 00:00:00 2001 From: Bram Jans Date: Fri, 8 Nov 2024 10:59:03 +0100 Subject: [PATCH 18/46] HOTFIX: Mobile offline setup zip query params --- iaso/tasks/utils/mobile_app_setup_api_calls.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/iaso/tasks/utils/mobile_app_setup_api_calls.py b/iaso/tasks/utils/mobile_app_setup_api_calls.py index a6b8fc4e97..cfee945ed6 100644 --- a/iaso/tasks/utils/mobile_app_setup_api_calls.py +++ b/iaso/tasks/utils/mobile_app_setup_api_calls.py @@ -4,12 +4,14 @@ "filename": "orgunittypes", }, { - "path": "/api/mobile/groups/?showDeleted=true", + "path": "/api/mobile/groups/", "filename": "groups", + "query_params": {"showDeleted": "true"}, }, { - "path": "/api/mobile/group_sets/?showDeleted=true", + "path": "/api/mobile/group_sets/", "filename": "groupsets", + "query_params": {"showDeleted": "true"}, }, { "path": "/api/mobile/forms/", From 7a3a97ac743a8a6a62d8588482c35c971b53222e Mon Sep 17 00:00:00 2001 From: HAKIZIMANA Franck Date: Fri, 8 Nov 2024 13:53:43 +0200 Subject: [PATCH 19/46] remove the instances_count param to be checked when requestin the instances_count for the orgUnit and its descendants --- .../orgUnits/hooks/requests/useGetOrgUnitDetail.ts | 9 +-------- iaso/api/org_units.py | 7 +++---- iaso/tests/api/test_orgunits.py | 4 ++-- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetOrgUnitDetail.ts b/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetOrgUnitDetail.ts index b74b658a12..c814834f9d 100644 --- a/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetOrgUnitDetail.ts +++ b/hat/assets/js/apps/Iaso/domains/orgUnits/hooks/requests/useGetOrgUnitDetail.ts @@ -6,20 +6,13 @@ import { useSnackQuery } from '../../../../libs/apiHooks'; import { OrgUnit } from '../../types/orgUnit'; import MESSAGES from '../../messages'; -import { makeUrlWithParams } from '../../../../libs/utils'; export const useGetOrgUnitDetail = ( id?: number, ): UseQueryResult => { - const params: Record = { - instances_count: true, - }; - - const url = makeUrlWithParams(`/api/orgunits/${id}/`, params); - return useSnackQuery({ queryKey: ['orgunitdetail', id], - queryFn: () => getRequest(url), + queryFn: () => getRequest(`/api/orgunits/${id}/`), snackErrorMsg: MESSAGES.fetchOrgUnitError, options: { enabled: Boolean(id), diff --git a/iaso/api/org_units.py b/iaso/api/org_units.py index a3aee84bd8..d19efc61a7 100644 --- a/iaso/api/org_units.py +++ b/iaso/api/org_units.py @@ -766,10 +766,9 @@ def retrieve(self, request, pk=None): self.get_queryset().prefetch_related("reference_instances"), pk=pk, ) - - if request.query_params.get("instances_count"): - instances_count = org_unit.descendants().aggregate(Count("instance"))["instance__count"] - org_unit.instances_count = instances_count + # Get instances count for the Org unit and its descendants + instances_count = org_unit.descendants().aggregate(Count("instance"))["instance__count"] + org_unit.instances_count = instances_count self.check_object_permissions(request, org_unit) res = org_unit.as_dict_with_parents(light=False, light_parents=False) diff --git a/iaso/tests/api/test_orgunits.py b/iaso/tests/api/test_orgunits.py index 80a6b0a2e6..5c06ee1c0e 100644 --- a/iaso/tests/api/test_orgunits.py +++ b/iaso/tests/api/test_orgunits.py @@ -551,13 +551,13 @@ def test_org_unit_retrieve_with_instances_count(self): json={"name": "b", "age": 19, "gender": "F"}, ) # Test the descendant instances count - response_descendant = self.client.get(f"/api/orgunits/{descendant_org_unit.id}/?instances_count=true/") + response_descendant = self.client.get(f"/api/orgunits/{descendant_org_unit.id}/") self.assertJSONResponse(response_descendant, 200) descendant_instances_count = response_descendant.json()["instances_count"] self.assertEquals(descendant_instances_count, 1) # Test the parent instances count - response_parent = self.client.get(f"/api/orgunits/{parent_org_unit.id}/?instances_count=true/") + response_parent = self.client.get(f"/api/orgunits/{parent_org_unit.id}/") self.assertJSONResponse(response_parent, 200) parent_instances_count = response_parent.json()["instances_count"] self.assertEquals(parent_instances_count, 2) From cf486f5b1eb66217ab852ef1ef9ff7147cea7903 Mon Sep 17 00:00:00 2001 From: Math VDH Date: Fri, 8 Nov 2024 12:17:58 +0000 Subject: [PATCH 20/46] Update plugins/polio/migrations/0202_populate_forma_round.py Fix by marc Co-authored-by: Marc Hertzog --- plugins/polio/migrations/0202_populate_forma_round.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/polio/migrations/0202_populate_forma_round.py b/plugins/polio/migrations/0202_populate_forma_round.py index 3d53dca483..05e8e6a549 100644 --- a/plugins/polio/migrations/0202_populate_forma_round.py +++ b/plugins/polio/migrations/0202_populate_forma_round.py @@ -66,4 +66,4 @@ class Migration(migrations.Migration): ("polio", "0201_outgoingstockmovement_round"), ] - operations = [migrations.RunPython(populate_forma_round, reverse_populate_forma_round)] + operations = [migrations.RunPython(populate_forma_round, reverse_populate_forma_round, elidable=True)] From 2128b778ca5f9943994e462b076f00e2744ef40b Mon Sep 17 00:00:00 2001 From: Math VDH Date: Fri, 8 Nov 2024 12:18:21 +0000 Subject: [PATCH 21/46] Update plugins/polio/js/src/domains/VaccineModule/StockManagement/hooks/api.ts Cleanup Co-authored-by: Marc Hertzog --- .../VaccineModule/StockManagement/hooks/api.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/hooks/api.ts b/plugins/polio/js/src/domains/VaccineModule/StockManagement/hooks/api.ts index 80ebae76a1..24bcae8aa9 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/hooks/api.ts +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/hooks/api.ts @@ -268,17 +268,6 @@ export const useCampaignOptions = ( if (!data) return []; return data.filter(c => c.top_level_org_unit_name === countryName); }, - // select: data => { - // if (!data) return []; - // return data - // .filter(c => c.top_level_org_unit_name === countryName) - // .map(c => { - // return { - // label: c.obr_name, - // value: c.obr_name, - // }; - // }); - // }, keepPreviousData: true, staleTime: 1000 * 60 * 15, // in MS cacheTime: 1000 * 60 * 5, From 29cdab5ccba949c49befb991cb8d14716ca6f7e2 Mon Sep 17 00:00:00 2001 From: Christophe Gerard Date: Fri, 8 Nov 2024 13:52:10 +0100 Subject: [PATCH 22/46] adapt it everywhere --- .../files/pdf/DocumentUploadWithPreview.tsx | 55 ++++++ .../Iaso/components/files/pdf/messages.ts | 10 ++ .../apps/Iaso/components/files/pdf/utils.ts | 11 ++ .../Modals/CreateEditDestruction.tsx | 36 ++-- .../StockVariation/Modals/CreateEditFormA.tsx | 32 ++-- .../Modals/CreateEditIncident.tsx | 57 ++---- .../StockVariation/Table/columns.tsx | 169 ++++++++++-------- .../Details/PreAlerts/PreAlert.tsx | 34 ++-- .../VaccineRequestForm/VaccineRequestForm.tsx | 38 ++-- .../SupplyChain/Details/shared.tsx | 6 +- .../SupplyChain/Details/utils.ts | 12 +- .../SupplyChain/hooks/api/vrf.tsx | 43 ++--- 12 files changed, 261 insertions(+), 242 deletions(-) create mode 100644 hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx create mode 100644 hat/assets/js/apps/Iaso/components/files/pdf/messages.ts diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx b/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx new file mode 100644 index 0000000000..cc18710f37 --- /dev/null +++ b/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx @@ -0,0 +1,55 @@ +import { Grid } from '@mui/material'; +import { FilesUpload, useSafeIntl } from 'bluesquare-components'; +import React from 'react'; +import { PdfPreview } from './PdfPreview'; +import MESSAGES from './messages'; +import { acceptPDF } from './utils'; + +type DocumentUploadWithPreviewProps = { + errors: string[] | undefined; + onFilesSelect: (files: File[]) => void; + document?: File[] | string; +}; + +const DocumentUploadWithPreview: React.FC = ({ + errors, + onFilesSelect, + document, +}) => { + const { formatMessage } = useSafeIntl(); + + let pdfUrl: string | null = null; + if (typeof document === 'string') { + pdfUrl = document; + } else if ( + Array.isArray(document) && + document.length > 0 && + document[0] instanceof File + ) { + pdfUrl = URL.createObjectURL(document[0]); + } else if (document instanceof File) { + pdfUrl = URL.createObjectURL(document[0]); + } + + return ( + + + + + {pdfUrl && ( + + + + )} + + ); +}; + +export default DocumentUploadWithPreview; diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts b/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts new file mode 100644 index 0000000000..9ec9291b9c --- /dev/null +++ b/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts @@ -0,0 +1,10 @@ +import { defineMessages } from 'react-intl'; + +const MESSAGES = defineMessages({ + document: { + id: 'iaso.polio.label.document', + defaultMessage: 'Document', + }, +}); + +export default MESSAGES; diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/utils.ts b/hat/assets/js/apps/Iaso/components/files/pdf/utils.ts index e69de29bb2..26e4e6034c 100644 --- a/hat/assets/js/apps/Iaso/components/files/pdf/utils.ts +++ b/hat/assets/js/apps/Iaso/components/files/pdf/utils.ts @@ -0,0 +1,11 @@ +import { Accept } from 'react-dropzone'; + +export const acceptPDF: Accept = { + 'application/pdf': ['.pdf'], +}; + +export const processErrorDocsBase = err_docs => { + if (!err_docs) return []; + if (!Array.isArray(err_docs)) return [err_docs]; + return err_docs; +}; diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditDestruction.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditDestruction.tsx index 18c696f270..cd68010966 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditDestruction.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditDestruction.tsx @@ -2,24 +2,21 @@ import { Box } from '@mui/material'; import { AddButton, ConfirmCancelModal, - FilesUpload, makeFullModal, useSafeIntl, } from 'bluesquare-components'; import { Field, FormikProvider, useFormik } from 'formik'; import { isEqual } from 'lodash'; -import React, { FunctionComponent, useCallback } from 'react'; +import React, { FunctionComponent, useMemo } from 'react'; import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; +import DocumentUploadWithPreview from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview'; +import { processErrorDocsBase } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/utils'; import { DateInput, NumberInput, TextInput, } from '../../../../../components/Inputs'; import { Vaccine } from '../../../../../constants/types'; -import { - acceptPDF, - processErrorDocsBase, -} from '../../../SupplyChain/Details/utils'; import { useSaveDestruction } from '../../hooks/api'; import MESSAGES from '../../messages'; import { useDestructionValidation } from './validation'; @@ -62,15 +59,14 @@ export const CreateEditDestruction: FunctionComponent = ({ validationSchema, }); - const processDocumentErrors = useCallback(processErrorDocsBase, [ - formik.errors, - ]); - const titleMessage = destruction?.id ? MESSAGES.edit : MESSAGES.create; const title = `${countryName} - ${vaccine}: ${formatMessage( titleMessage, )} ${formatMessage(MESSAGES.destructionReports)}`; const allowConfirm = formik.isValid && !isEqual(formik.touched, {}); + const documentErrors = useMemo(() => { + return processErrorDocsBase(formik.errors.document); + }, [formik.errors.document]); return ( @@ -138,22 +134,15 @@ export const CreateEditDestruction: FunctionComponent = ({ /> */} - { if (files.length) { - formik.setFieldTouched(`document`, true); - formik.setFieldValue(`document`, files); + formik.setFieldTouched('document', true); + formik.setFieldValue('document', files); } }} - multi={false} - errors={processDocumentErrors(formik.errors.document)} - placeholder={formatMessage(MESSAGES.document)} + document={formik.values.document} /> @@ -165,6 +154,5 @@ const modalWithIcon = makeFullModal(CreateEditDestruction, EditIconButton); export { modalWithButton as CreateDestruction, - modalWithIcon as EditDestruction + modalWithIcon as EditDestruction, }; - diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx index ae25b88fb8..15804adfb6 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditFormA.tsx @@ -2,14 +2,15 @@ import { Box } from '@mui/material'; import { AddButton, ConfirmCancelModal, - FilesUpload, makeFullModal, useSafeIntl, } from 'bluesquare-components'; import { Field, FormikProvider, useFormik } from 'formik'; import { isEqual } from 'lodash'; -import React, { FunctionComponent, useCallback } from 'react'; +import React, { FunctionComponent, useMemo } from 'react'; import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; +import DocumentUploadWithPreview from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview'; +import { processErrorDocsBase } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/utils'; import { DateInput, NumberInput, @@ -17,10 +18,6 @@ import { } from '../../../../../components/Inputs'; import { SingleSelect } from '../../../../../components/Inputs/SingleSelect'; import { Vaccine } from '../../../../../constants/types'; -import { - acceptPDF, - processErrorDocsBase, -} from '../../../SupplyChain/Details/utils'; import { useCampaignOptions, useSaveFormA } from '../../hooks/api'; import MESSAGES from '../../messages'; import { useFormAValidation } from './validation'; @@ -63,9 +60,6 @@ export const CreateEditFormA: FunctionComponent = ({ onSubmit: values => save(values), validationSchema, }); - const processDocumentErrors = useCallback(processErrorDocsBase, [ - formik.errors, - ]); const { data: campaignOptions, isFetching: isFetchingCampaigns } = useCampaignOptions(countryName, formik.values.campaign); @@ -74,6 +68,9 @@ export const CreateEditFormA: FunctionComponent = ({ titleMessage, )} ${formatMessage(MESSAGES.formA)}`; const allowConfirm = formik.isValid && !isEqual(formik.touched, {}); + const documentErrors = useMemo(() => { + return processErrorDocsBase(formik.errors.document); + }, [formik.errors.document]); return ( @@ -142,22 +139,15 @@ export const CreateEditFormA: FunctionComponent = ({ /> - { if (files.length) { - formik.setFieldTouched(`document`, true); - formik.setFieldValue(`document`, files); + formik.setFieldTouched('document', true); + formik.setFieldValue('document', files); } }} - multi={false} - errors={processDocumentErrors(formik.errors.document)} - placeholder={formatMessage(MESSAGES.document)} + document={formik.values.document} /> diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditIncident.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditIncident.tsx index 04d46f92e5..5661406cc1 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditIncident.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Modals/CreateEditIncident.tsx @@ -2,7 +2,6 @@ import { Box, FormControl, FormControlLabel, - Grid, Radio, RadioGroup, Typography, @@ -10,7 +9,6 @@ import { import { AddButton, ConfirmCancelModal, - FilesUpload, makeFullModal, useSafeIntl, } from 'bluesquare-components'; @@ -18,7 +16,8 @@ import { Field, FormikProvider, useFormik } from 'formik'; import { isEqual } from 'lodash'; import React, { FunctionComponent, useCallback, useMemo } from 'react'; import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; -import { PdfPreview } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview'; +import DocumentUploadWithPreview from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview'; +import { processErrorDocsBase } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/utils'; import { DateInput, NumberInput, @@ -26,10 +25,6 @@ import { } from '../../../../../components/Inputs'; import { SingleSelect } from '../../../../../components/Inputs/SingleSelect'; import { Vaccine } from '../../../../../constants/types'; -import { - acceptPDF, - processErrorDocsBase, -} from '../../../SupplyChain/Details/utils'; import { useSaveIncident } from '../../hooks/api'; import { useGetMovementDescription } from '../../hooks/useGetMovementDescription'; import MESSAGES from '../../messages'; @@ -233,9 +228,10 @@ export const CreateEditIncident: FunctionComponent = ({ validationSchema, }); - const processDocumentErrors = useCallback(processErrorDocsBase, [ - formik.errors, - ]); + const documentErrors = useMemo(() => { + return processErrorDocsBase(formik.errors.document); + }, [formik.errors.document]); + const incidentTypeOptions = useIncidentOptions(); const titleMessage = incident?.id ? MESSAGES.edit : MESSAGES.create; const title = `${countryName} - ${vaccine}: ${formatMessage( @@ -405,37 +401,16 @@ export const CreateEditIncident: FunctionComponent = ({ /> - - - { - if (files.length) { - formik.setFieldTouched( - `document`, - true, - ); - formik.setFieldValue(`document`, files); - } - }} - multi={false} - errors={processDocumentErrors( - formik.errors.document, - )} - placeholder={formatMessage(MESSAGES.document)} - /> - - {incident.document && ( - - - - )} - + { + if (files.length) { + formik.setFieldTouched('document', true); + formik.setFieldValue('document', files); + } + }} + document={formik.values.document} + /> diff --git a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Table/columns.tsx b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Table/columns.tsx index 0d540e9749..d7da64d82c 100644 --- a/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Table/columns.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/StockManagement/StockVariation/Table/columns.tsx @@ -5,8 +5,6 @@ import { DateCell } from '../../../../../../../../../hat/assets/js/apps/Iaso/com import { NumberCell } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Cells/NumberCell'; import DeleteDialog from '../../../../../../../../../hat/assets/js/apps/Iaso/components/dialogs/DeleteDialogComponent'; import { PdfPreview } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview'; -import { userHasPermission } from '../../../../../../../../../hat/assets/js/apps/Iaso/domains/users/utils'; -import { useCurrentUser } from '../../../../../../../../../hat/assets/js/apps/Iaso/utils/usersUtils'; import { STOCK_MANAGEMENT_WRITE } from '../../../../../constants/permissions'; import { Vaccine } from '../../../../../constants/types'; import MESSAGES from '../../messages'; @@ -15,6 +13,7 @@ import { EditFormA } from '../Modals/CreateEditFormA'; import { EditIncident } from '../Modals/CreateEditIncident'; import { BreakWordCell } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Cells/BreakWordCell'; +import { DisplayIfUserHasPerm } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/DisplayIfUserHasPerm'; import { useDeleteDestruction, useDeleteFormA, @@ -26,7 +25,6 @@ export const useFormATableColumns = ( vaccine: Vaccine, ): Column[] => { const { formatMessage } = useSafeIntl(); - const currentUser = useCurrentUser(); const { mutateAsync: deleteFormA } = useDeleteFormA(); return useMemo(() => { @@ -87,9 +85,7 @@ export const useFormATableColumns = ( return textPlaceholder; }, }, - ]; - if (userHasPermission(STOCK_MANAGEMENT_WRITE, currentUser)) { - columns.push({ + { Header: formatMessage(MESSAGES.actions), id: 'account', accessor: 'account', @@ -97,37 +93,47 @@ export const useFormATableColumns = ( Cell: settings => { return ( <> - - - deleteFormA(settings.row.original.id) - } + + + <> + + + deleteFormA( + settings.row.original.id, + ) + } + /> + + ); }, - }); - } + }, + ]; return columns; - }, [formatMessage, currentUser, countryName, vaccine, deleteFormA]); + }, [formatMessage, countryName, vaccine, deleteFormA]); }; export const useDestructionTableColumns = ( countryName: string, vaccine: Vaccine, ): Column[] => { const { formatMessage } = useSafeIntl(); - const currentUser = useCurrentUser(); const { mutateAsync: deleteDestruction } = useDeleteDestruction(); return useMemo(() => { @@ -165,9 +171,7 @@ export const useDestructionTableColumns = ( /> ), }, - ]; - if (userHasPermission(STOCK_MANAGEMENT_WRITE, currentUser)) { - columns.push({ + { Header: formatMessage(MESSAGES.actions), accessor: 'account', id: 'account', @@ -175,37 +179,51 @@ export const useDestructionTableColumns = ( Cell: settings => { return ( <> - - - deleteDestruction(settings.row.original.id) - } + + + <> + + + deleteDestruction( + settings.row.original.id, + ) + } + /> + + ); }, - }); - } + }, + ]; return columns; - }, [countryName, formatMessage, vaccine, currentUser, deleteDestruction]); + }, [countryName, formatMessage, vaccine, deleteDestruction]); }; export const useIncidentTableColumns = ( countryName: string, vaccine: Vaccine, ): Column[] => { const { formatMessage } = useSafeIntl(); - const currentUser = useCurrentUser(); const { mutateAsync: deleteIncident } = useDeleteIncident(); return useMemo(() => { const columns = [ @@ -260,9 +278,7 @@ export const useIncidentTableColumns = ( ), }, - ]; - if (userHasPermission(STOCK_MANAGEMENT_WRITE, currentUser)) { - columns.push({ + { Header: formatMessage(MESSAGES.actions), accessor: 'account', id: 'account', @@ -273,28 +289,37 @@ export const useIncidentTableColumns = ( - - - deleteIncident(settings.row.original.id) - } - /> + + + <> + + + deleteIncident( + settings.row.original.id, + ) + } + /> + + ); }, - }); - } + }, + ]; return columns; - }, [countryName, formatMessage, vaccine, currentUser, deleteIncident]); + }, [countryName, formatMessage, vaccine, deleteIncident]); }; diff --git a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/PreAlerts/PreAlert.tsx b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/PreAlerts/PreAlert.tsx index 03a901d5b1..db29615ef4 100644 --- a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/PreAlerts/PreAlert.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/PreAlerts/PreAlert.tsx @@ -1,11 +1,13 @@ import RestoreFromTrashIcon from '@mui/icons-material/RestoreFromTrash'; import { Box, Grid, Paper, Typography } from '@mui/material'; -import { FilesUpload, IconButton, useSafeIntl } from 'bluesquare-components'; +import { IconButton, useSafeIntl } from 'bluesquare-components'; import classNames from 'classnames'; import { Field, useFormikContext } from 'formik'; -import React, { FunctionComponent, useCallback } from 'react'; +import React, { FunctionComponent, useCallback, useMemo } from 'react'; import { DeleteIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/DeleteIconButton'; import { NumberCell } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Cells/NumberCell'; +import DocumentUploadWithPreview from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview'; +import { processErrorDocsBase } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/utils'; import { Optional } from '../../../../../../../../../hat/assets/js/apps/Iaso/types/utils'; import { NumberInput, TextInput } from '../../../../../components/Inputs'; import { DateInput } from '../../../../../components/Inputs/DateInput'; @@ -13,7 +15,6 @@ import { dosesPerVial } from '../../hooks/utils'; import MESSAGES from '../../messages'; import { SupplyChainFormData } from '../../types'; import { grayText, usePaperStyles } from '../shared'; -import { acceptPDF, processErrorDocsBase } from '../utils'; type Props = { index: number; @@ -38,8 +39,9 @@ export const PreAlert: FunctionComponent = ({ index, vaccine }) => { ) : 0; - const processDocumentErrors = useCallback(processErrorDocsBase, [errors]); - + const documentErrors = useMemo(() => { + return processErrorDocsBase(errors[index]?.document); + }, [errors, index]); const onDelete = useCallback(() => { if (values?.pre_alerts?.[index].id) { setFieldValue(`pre_alerts[${index}].to_delete`, true); @@ -147,16 +149,8 @@ export const PreAlert: FunctionComponent = ({ index, vaccine }) => { - { if (files.length) { setFieldTouched( @@ -169,13 +163,9 @@ export const PreAlert: FunctionComponent = ({ index, vaccine }) => { ); } }} - multi={false} - errors={processDocumentErrors( - errors[index]?.document, - )} - placeholder={formatMessage( - MESSAGES.document, - )} + document={ + values?.pre_alerts?.[index]?.document + } /> diff --git a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx index 886ef014fb..76f6bb2d22 100644 --- a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/VaccineRequestForm/VaccineRequestForm.tsx @@ -1,7 +1,14 @@ import { Box, Grid, Typography } from '@mui/material'; -import { FilesUpload, useSafeIntl } from 'bluesquare-components'; +import { useSafeIntl } from 'bluesquare-components'; import { Field, useFormikContext } from 'formik'; -import React, { FunctionComponent, useCallback, useEffect } from 'react'; +import React, { + FunctionComponent, + useCallback, + useEffect, + useMemo, +} from 'react'; +import DocumentUploadWithPreview from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview'; +import { processErrorDocsBase } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/files/pdf/utils'; import { TextArea } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/forms/TextArea'; import { NumberInput } from '../../../../../components/Inputs'; import { DateInput } from '../../../../../components/Inputs/DateInput'; @@ -15,7 +22,6 @@ import { import { useSkipEffectUntilValue } from '../../hooks/utils'; import MESSAGES from '../../messages'; import { useSharedStyles } from '../shared'; -import { acceptPDF, processErrorDocsBase } from '../utils'; type Props = { className?: string; vrfData: any }; @@ -90,8 +96,9 @@ export const VaccineRequestForm: FunctionComponent = ({ const isNormalType = values?.vrf?.vrf_type === 'Normal'; - const processDocumentErrors = useCallback(processErrorDocsBase, [errors]); - + const documentErrors = useMemo(() => { + return processErrorDocsBase(errors.document); + }, [errors.document]); return ( @@ -310,13 +317,8 @@ export const VaccineRequestForm: FunctionComponent = ({ - { if (files.length) { setFieldTouched( @@ -328,18 +330,8 @@ export const VaccineRequestForm: FunctionComponent = ({ files, ); } - console.log( - `File selected :${files.length}`, - ); - console.dir(files); }} - multi={false} - errors={processDocumentErrors( - errors.document, - )} - placeholder={formatMessage( - MESSAGES.document, - )} + document={values?.vrf?.document} /> diff --git a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/shared.tsx b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/shared.tsx index 13327b277f..2caf51ad6f 100644 --- a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/shared.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/shared.tsx @@ -1,5 +1,5 @@ -import React, { FunctionComponent, ReactNode } from 'react'; import { Box, Grid, Theme, Typography } from '@mui/material'; +import { grey } from '@mui/material/colors'; import { makeStyles } from '@mui/styles'; import { AddButton, @@ -7,11 +7,11 @@ import { MENU_HEIGHT_WITH_TABS, useSafeIntl, } from 'bluesquare-components'; -import { grey } from '@mui/material/colors'; +import React, { FunctionComponent, ReactNode } from 'react'; export const useSharedStyles = makeStyles({ scrollableForm: { - height: `calc(100vh - ${MENU_HEIGHT_WITH_TABS + 200}px)`, + height: `calc(100vh - ${MENU_HEIGHT_WITH_TABS + 250}px)`, overflowY: 'auto', }, }); diff --git a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/utils.ts b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/utils.ts index 940a765eb4..d161df4548 100644 --- a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/utils.ts +++ b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/Details/utils.ts @@ -1,5 +1,4 @@ import { FormikErrors, FormikProps, FormikTouched } from 'formik'; -import { Accept } from 'react-dropzone'; import { baseUrls } from '../../../../constants/urls'; import { VRF } from '../constants'; import { @@ -191,6 +190,7 @@ export const makeHandleSubmit = setInitialValues(newInitialValues); formik.setErrors(newErrors); formik.setTouched(newTouched); + console.log('newValues', newValues); formik.setValues(newValues); } }, @@ -200,13 +200,3 @@ export const makeHandleSubmit = }, ); }; - -export const acceptPDF: Accept = { - 'application/pdf': ['.pdf'], -}; - -export const processErrorDocsBase = err_docs => { - if (!err_docs) return []; - if (!Array.isArray(err_docs)) return [err_docs]; - return err_docs; -}; diff --git a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/hooks/api/vrf.tsx b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/hooks/api/vrf.tsx index 1838366197..2d456be150 100644 --- a/plugins/polio/js/src/domains/VaccineModule/SupplyChain/hooks/api/vrf.tsx +++ b/plugins/polio/js/src/domains/VaccineModule/SupplyChain/hooks/api/vrf.tsx @@ -3,6 +3,8 @@ import { renderTagsWithTooltip, textPlaceholder, } from 'bluesquare-components'; +import { PostArg } from 'hat/assets/js/apps/Iaso/types/general'; +import moment from 'moment'; import { useMemo } from 'react'; import { UseMutationResult, UseQueryResult } from 'react-query'; import { openSnackBar } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/snackBars/EventDispatcher'; @@ -19,8 +21,6 @@ import { deleteRequest, getRequest, iasoFetch, - patchRequest, - postRequest, } from '../../../../../../../../../hat/assets/js/apps/Iaso/libs/Api'; import { useSnackMutation, @@ -47,8 +47,6 @@ import { VRF, VRFFormData, } from '../../types'; -import moment from 'moment'; -import { PostArg } from 'hat/assets/js/apps/Iaso/types/general'; const defaults = { order: '-start_date', @@ -185,17 +183,9 @@ export const useGetVrfDetails = (id?: string): UseQueryResult => { }); }; -const getRoundsForApi = ( - rounds: number[] | string | undefined, -): { number: number }[] | undefined => { - if (!rounds) return undefined; - if (Array.isArray(rounds)) return rounds.map(r => ({ number: r })); - return rounds.split(',').map(r => ({ number: parseInt(r, 10) })); -}; - const createFormDataRequest = ( method: 'PATCH' | 'POST', - arg1: PostArg + arg1: PostArg, ): Promise => { const { url, data = {}, fileData = {}, signal = null } = arg1; @@ -208,7 +198,7 @@ const createFormDataRequest = ( formData.append(key, converted_value); }); - let init: Record = { + const init: Record = { method, body: formData, signal, @@ -229,7 +219,7 @@ const createFormDataRequest = ( formData.append(key, value); } }); - } + } return iasoFetch(url, init).then(response => response.json()); }; @@ -247,18 +237,21 @@ export const saveVrf = ( ): Promise[] => { const filteredParams = vrf ? Object.fromEntries( - Object.entries(vrf).filter( - ([key, value]) => value !== undefined && value !== null && key !== 'document', - ), - ) + Object.entries(vrf).filter( + ([key, value]) => + value !== undefined && + value !== null && + key !== 'document', + ), + ) : {}; - const {rounds} = filteredParams; - if(Array.isArray(rounds)){ - if(rounds.length >0){ - filteredParams.rounds = rounds.join(',') - }else{ - filteredParams.rounds="" + const { rounds } = filteredParams; + if (Array.isArray(rounds)) { + if (rounds.length > 0) { + filteredParams.rounds = rounds.join(','); + } else { + filteredParams.rounds = ''; } } const requestBody: any = { From 3b0ca0ae01b37240c6e8103b28b937d10bab5d6e Mon Sep 17 00:00:00 2001 From: Christophe Gerard Date: Fri, 8 Nov 2024 14:22:34 +0100 Subject: [PATCH 23/46] added prod webpack config too --- hat/webpack.prod.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/hat/webpack.prod.js b/hat/webpack.prod.js index d1a9b56e51..a93e9b5580 100644 --- a/hat/webpack.prod.js +++ b/hat/webpack.prod.js @@ -215,6 +215,11 @@ module.exports = { filename: 'videos/[name].[hash][ext]', }, }, + { + test: /\.mjs$/, + type: 'javascript/auto', + use: 'babel-loader', + }, ], noParse: [require.resolve('typescript/lib/typescript.js')], // remove warning: https://github.com/microsoft/TypeScript/issues/39436 }, @@ -224,11 +229,14 @@ module.exports = { externals: [{ './cptable': 'var cptable' }], resolve: { + alias: { + 'react/jsx-runtime': 'react/jsx-runtime.js', + }, fallback: { fs: false, }, /* assets/js/apps path allow using absolute import eg: from 'iaso/libs/Api' */ modules: ['node_modules', path.resolve(__dirname, 'assets/js/apps/')], - extensions: ['.js', '.tsx', '.ts'], + extensions: ['.js', '.jsx', '.ts', '.tsx'], }, }; From 7a0096f3b857a07e0f35c11fc95189bdda447fda Mon Sep 17 00:00:00 2001 From: Christophe Gerard Date: Fri, 8 Nov 2024 14:59:31 +0100 Subject: [PATCH 24/46] micking libraries for test js --- hat/assets/js/test/helpers.js | 7 ++++--- hat/assets/js/test/utils/pdf.js | 32 ++++++++++++++++++++++++++++++++ hat/webpack.dev.js | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 hat/assets/js/test/utils/pdf.js diff --git a/hat/assets/js/test/helpers.js b/hat/assets/js/test/helpers.js index 9bb0fe6c85..db649a4bed 100644 --- a/hat/assets/js/test/helpers.js +++ b/hat/assets/js/test/helpers.js @@ -1,10 +1,11 @@ -import React from 'react'; -import sinon from 'sinon'; +import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import { expect } from 'chai'; import { configure, mount, render, shallow } from 'enzyme'; -import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import nodeFetch from 'node-fetch'; +import React from 'react'; +import sinon from 'sinon'; import { mockMessages } from './utils/intl'; +import './utils/pdf'; import { baseUrl as baseUrlConst } from './utils/requests'; configure({ adapter: new Adapter() }); diff --git a/hat/assets/js/test/utils/pdf.js b/hat/assets/js/test/utils/pdf.js new file mode 100644 index 0000000000..5307031182 --- /dev/null +++ b/hat/assets/js/test/utils/pdf.js @@ -0,0 +1,32 @@ +// test/setup.js or in your test file +const mock = require('mock-require'); + + // Mock pdfjs-dist +mock('pdfjs-dist', { + GlobalWorkerOptions: { + workerSrc: '', + }, + getDocument: () => ({ + promise: Promise.resolve({ + numPages: 1, + getPage: () => ({ + promise: Promise.resolve({ + getTextContent: () => ({ + promise: Promise.resolve({ items: [] }), + }), + }), + }), + }), + }), +}); + +// Mock react-pdf +mock('react-pdf', { + Document: ({ children }) =>
{children}
, + Page: () =>
Page
, + pdfjs: { + GlobalWorkerOptions: { + workerSrc: '', + }, + }, +}); \ No newline at end of file diff --git a/hat/webpack.dev.js b/hat/webpack.dev.js index 15da577d6c..1bc695cfe4 100644 --- a/hat/webpack.dev.js +++ b/hat/webpack.dev.js @@ -267,7 +267,7 @@ module.exports = { { test: /\.mjs$/, type: 'javascript/auto', - use: 'babel-loader', // or any other loader you are using + use: 'babel-loader', }, ], noParse: [require.resolve('typescript/lib/typescript.js')], // remove warning: https://github.com/microsoft/TypeScript/issues/39436 From 5c323a64bd0defb4d151a8761d6db56e67503af6 Mon Sep 17 00:00:00 2001 From: Christophe Gerard Date: Fri, 8 Nov 2024 15:05:34 +0100 Subject: [PATCH 25/46] code review --- .../files/pdf/DocumentUploadWithPreview.tsx | 2 +- .../Iaso/components/files/pdf/PdfPreview.tsx | 17 +++-------------- .../apps/Iaso/components/files/pdf/messages.ts | 8 ++++++++ hat/assets/js/test/utils/pdf.js | 4 ++-- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx b/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx index cc18710f37..052c805334 100644 --- a/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx +++ b/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx @@ -28,7 +28,7 @@ const DocumentUploadWithPreview: React.FC = ({ ) { pdfUrl = URL.createObjectURL(document[0]); } else if (document instanceof File) { - pdfUrl = URL.createObjectURL(document[0]); + pdfUrl = URL.createObjectURL(document); } return ( diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview.tsx b/hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview.tsx index 1d726a77ad..6e6a4d39b4 100644 --- a/hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview.tsx +++ b/hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview.tsx @@ -6,10 +6,10 @@ import { IconButton, } from '@mui/material'; import { useSafeIntl } from 'bluesquare-components'; -import React, { useCallback, useState } from 'react'; -import { defineMessages } from 'react-intl'; +import React, { FunctionComponent, useCallback, useState } from 'react'; import { Document, Page, pdfjs } from 'react-pdf'; import PdfSvgComponent from '../../svg/PdfSvgComponent'; +import MESSAGES from './messages'; // Set the workerSrc for pdfjs to enable the use of Web Workers. // Web Workers allow the PDF.js library to process PDF files in a separate thread, @@ -24,18 +24,7 @@ type PdfPreviewProps = { pdfUrl?: string; }; -export const MESSAGES = defineMessages({ - close: { - defaultMessage: 'Close', - id: 'blsq.buttons.label.close', - }, - download: { - defaultMessage: 'Download', - id: 'blsq.buttons.label.download', - }, -}); - -export const PdfPreview: React.FC = ({ pdfUrl }) => { +export const PdfPreview: FunctionComponent = ({ pdfUrl }) => { const [open, setOpen] = useState(false); // State to manage dialog open/close const { formatMessage } = useSafeIntl(); diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts b/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts index 9ec9291b9c..e0db8523c4 100644 --- a/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts +++ b/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts @@ -5,6 +5,14 @@ const MESSAGES = defineMessages({ id: 'iaso.polio.label.document', defaultMessage: 'Document', }, + close: { + defaultMessage: 'Close', + id: 'blsq.buttons.label.close', + }, + download: { + defaultMessage: 'Download', + id: 'iaso.label.download', + }, }); export default MESSAGES; diff --git a/hat/assets/js/test/utils/pdf.js b/hat/assets/js/test/utils/pdf.js index 5307031182..4e11b564a8 100644 --- a/hat/assets/js/test/utils/pdf.js +++ b/hat/assets/js/test/utils/pdf.js @@ -1,7 +1,7 @@ // test/setup.js or in your test file const mock = require('mock-require'); - // Mock pdfjs-dist +// Mock pdfjs-dist mock('pdfjs-dist', { GlobalWorkerOptions: { workerSrc: '', @@ -29,4 +29,4 @@ mock('react-pdf', { workerSrc: '', }, }, -}); \ No newline at end of file +}); From 4a2de439af37c1cbdc5664911ff2361a5e393af6 Mon Sep 17 00:00:00 2001 From: Christophe Gerard Date: Fri, 8 Nov 2024 15:18:44 +0100 Subject: [PATCH 26/46] translatfixing translationsion issues --- hat/assets/js/apps/Iaso/components/files/pdf/messages.ts | 2 +- hat/assets/js/apps/Iaso/domains/app/translations/en.json | 2 ++ hat/assets/js/apps/Iaso/domains/app/translations/fr.json | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts b/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts index e0db8523c4..6f4d8e1605 100644 --- a/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts +++ b/hat/assets/js/apps/Iaso/components/files/pdf/messages.ts @@ -2,7 +2,7 @@ import { defineMessages } from 'react-intl'; const MESSAGES = defineMessages({ document: { - id: 'iaso.polio.label.document', + id: 'iaso.label.document', defaultMessage: 'Document', }, close: { diff --git a/hat/assets/js/apps/Iaso/domains/app/translations/en.json b/hat/assets/js/apps/Iaso/domains/app/translations/en.json index a42680ab7d..01c7b0cb19 100644 --- a/hat/assets/js/apps/Iaso/domains/app/translations/en.json +++ b/hat/assets/js/apps/Iaso/domains/app/translations/en.json @@ -490,6 +490,7 @@ "iaso.label.display": "Display", "iaso.label.displayPassword": "Display the password", "iaso.label.docs": "Docs", + "iaso.label.document": "Document", "iaso.label.documents": "Documents", "iaso.label.done": "Done", "iaso.label.download": "Download", @@ -1106,6 +1107,7 @@ "iaso.planning.title": "Planning", "iaso.plannings.label.duplicatePlanning": "Duplicate planning", "iaso.plannings.label.selectOrgUnit": "Please select org unit", + "iaso.polio.label.document": "Document CHECKME", "iaso.projects.appId": "App ID", "iaso.projects.create": "Create project", "iaso.projects.false": "User doesn't need authentication", diff --git a/hat/assets/js/apps/Iaso/domains/app/translations/fr.json b/hat/assets/js/apps/Iaso/domains/app/translations/fr.json index 353bdf769f..dba9e84b8e 100644 --- a/hat/assets/js/apps/Iaso/domains/app/translations/fr.json +++ b/hat/assets/js/apps/Iaso/domains/app/translations/fr.json @@ -490,6 +490,7 @@ "iaso.label.display": "Afficher", "iaso.label.displayPassword": "Afficher le mot de passe", "iaso.label.docs": "Docs", + "iaso.label.document": "Document", "iaso.label.documents": "Documents", "iaso.label.done": "Terminé", "iaso.label.download": "Télécharger", @@ -1106,6 +1107,7 @@ "iaso.planning.title": "Planning", "iaso.plannings.label.duplicatePlanning": "Dupliquer planning", "iaso.plannings.label.selectOrgUnit": "Sélectionner une unité d'org.", + "iaso.polio.label.document": "Document CHECKME", "iaso.projects.appId": "Identifiant de l'App", "iaso.projects.create": "Créer un projet", "iaso.projects.false": "L'utilisateur n'a pas besoin d'authentification", From 0d07f1c19b60e18f32d7ce5e5a9fe7cb49788b4e Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Mon, 4 Nov 2024 09:35:00 +0200 Subject: [PATCH 27/46] Integrating account field in beneficiary table --- plugins/wfp/admin.py | 4 +-- .../wfp/management/commands/wfp_etl_Under5.py | 26 +++++++------------ .../wfp/management/commands/wfp_etl_pbwg.py | 25 +++++++----------- .../migrations/0013_beneficiary_account.py | 21 +++++++++++++++ plugins/wfp/models.py | 3 ++- 5 files changed, 43 insertions(+), 36 deletions(-) create mode 100644 plugins/wfp/migrations/0013_beneficiary_account.py diff --git a/plugins/wfp/admin.py b/plugins/wfp/admin.py index 4c9312ccdd..12fe8edbb5 100644 --- a/plugins/wfp/admin.py +++ b/plugins/wfp/admin.py @@ -10,8 +10,8 @@ @admin.register(Beneficiary) class BeneficiaryAdmin(admin.ModelAdmin): - list_filter = ("birth_date", "gender") - list_display = ("id", "birth_date", "gender") + list_filter = ("birth_date", "gender", "account") + list_display = ("id", "birth_date", "gender", "account") @admin.register(Journey) diff --git a/plugins/wfp/management/commands/wfp_etl_Under5.py b/plugins/wfp/management/commands/wfp_etl_Under5.py index 76d85c58e9..0899477e94 100644 --- a/plugins/wfp/management/commands/wfp_etl_Under5.py +++ b/plugins/wfp/management/commands/wfp_etl_Under5.py @@ -135,27 +135,16 @@ def group_visit_by_entity(self, entities): ) def journeyMapper(self, visits): - journey = [] current_journey = {"visits": [], "steps": []} anthropometric_visit_forms = [ "child_antropometric_followUp_tsfp", "child_antropometric_followUp_otp", ] - for index, visit in enumerate(visits): - if visit: - current_journey["weight_gain"] = visit.get("weight_gain", None) - current_journey["weight_loss"] = visit.get("weight_loss", None) - if visit.get("duration", None) is not None and visit.get("duration", None) != "": - current_journey["duration"] = visit.get("duration") - - if visit["form_id"] == "Anthropometric visit child": - current_journey["nutrition_programme"] = ETL().program_mapper(visit) - - current_journey = ETL().journey_Formatter( - visit, "Anthropometric visit child", anthropometric_visit_forms, current_journey, visits, index - ) - current_journey["steps"].append(visit) - journey.append(current_journey) + admission_form = "Anthropometric visit child" + visit_nutrition_program = [visit for visit in visits if visit["form_id"] == admission_form] + if len(visit_nutrition_program) > 0: + current_journey["nutrition_programme"] = ETL().program_mapper(visit_nutrition_program[0]) + journey = ETL().entity_journey_mapper(visits, anthropometric_visit_forms, admission_form, current_journey) return journey def save_journey(self, beneficiary, record): @@ -185,7 +174,9 @@ def save_journey(self, beneficiary, record): return journey def run(self): - beneficiaries = ETL("child_under_5_1").retrieve_entities() + entity_type = ETL("child_under_5_1") + account = entity_type.account_related_to_entity_type() + beneficiaries = entity_type.retrieve_entities() logger.info(f"Instances linked to Child Under 5 program: {beneficiaries.count()}") entities = sorted(list(beneficiaries), key=itemgetter("entity_id")) existing_beneficiaries = ETL().existing_beneficiaries() @@ -201,6 +192,7 @@ def run(self): beneficiary.gender = instance["gender"] beneficiary.birth_date = instance["birth_date"] beneficiary.entity_id = instance["entity_id"] + beneficiary.account = account beneficiary.save() logger.info(f"Created new beneficiary") else: diff --git a/plugins/wfp/management/commands/wfp_etl_pbwg.py b/plugins/wfp/management/commands/wfp_etl_pbwg.py index 4439e92c3f..f248ac8dee 100644 --- a/plugins/wfp/management/commands/wfp_etl_pbwg.py +++ b/plugins/wfp/management/commands/wfp_etl_pbwg.py @@ -10,7 +10,9 @@ class PBWG: def run(self): - beneficiaries = ETL("pbwg_1").retrieve_entities() + entity_type = ETL("pbwg_1") + account = entity_type.account_related_to_entity_type() + beneficiaries = entity_type.retrieve_entities() logger.info(f"Instances linked to PBWG program: {beneficiaries.count()}") entities = sorted(list(beneficiaries), key=itemgetter("entity_id")) existing_beneficiaries = ETL().existing_beneficiaries() @@ -25,6 +27,7 @@ def run(self): if instance["entity_id"] not in existing_beneficiaries and len(instance["journey"][0]["visits"]) > 0: beneficiary.gender = "" beneficiary.entity_id = instance["entity_id"] + beneficiary.account = account if instance.get("birth_date") is not None: beneficiary.birth_date = instance["birth_date"] beneficiary.save() @@ -72,26 +75,16 @@ def save_journey(self, beneficiary, record): return journey def journeyMapper(self, visits): - journey = [] current_journey = {"visits": [], "steps": []} anthropometric_visit_forms = [ "wfp_coda_pbwg_luctating_followup_anthro", "wfp_coda_pbwg_followup_anthro", ] - - for index, visit in enumerate(visits): - if visit: - if visit.get("duration", None) is not None and visit.get("duration", None) != "": - current_journey["duration"] = visit.get("duration") - - if visit["form_id"] == "wfp_coda_pbwg_registration": - current_journey["nutrition_programme"] = visit.get("physiology_status", None) - - current_journey = ETL().journey_Formatter( - visit, "wfp_coda_pbwg_anthropometric", anthropometric_visit_forms, current_journey, visits, index - ) - current_journey["steps"].append(visit) - journey.append(current_journey) + admission_form = "wfp_coda_pbwg_anthropometric" + visit_nutrition_program = [visit for visit in visits if visit["form_id"] == "wfp_coda_pbwg_registration"][0] + if len(visit_nutrition_program) > 0: + current_journey["nutrition_programme"] = visit_nutrition_program.get("physiology_status", None) + journey = ETL().entity_journey_mapper(visits, anthropometric_visit_forms, admission_form, current_journey) return journey def group_visit_by_entity(self, entities): diff --git a/plugins/wfp/migrations/0013_beneficiary_account.py b/plugins/wfp/migrations/0013_beneficiary_account.py new file mode 100644 index 0000000000..7a4209cd79 --- /dev/null +++ b/plugins/wfp/migrations/0013_beneficiary_account.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-11-02 08:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("iaso", "0307_merge_20241024_1145"), + ("wfp", "0012_rename_weight_difference_journey_initial_weight_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="beneficiary", + name="account", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="iaso.account" + ), + ), + ] diff --git a/plugins/wfp/models.py b/plugins/wfp/models.py index d016684961..df66db5b34 100644 --- a/plugins/wfp/models.py +++ b/plugins/wfp/models.py @@ -1,6 +1,6 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from iaso.models import OrgUnit +from iaso.models import OrgUnit, Account from plugins.wfp.models import * GENDERS = [("Male", _("Male")), ("Female", _("Female"))] @@ -54,6 +54,7 @@ class Beneficiary(models.Model): birth_date = models.DateField() gender = models.CharField(max_length=8, choices=GENDERS, null=True, blank=True) entity_id = models.IntegerField(null=True, blank=True) + account = models.ForeignKey(Account, on_delete=models.CASCADE, null=True, blank=True) class Journey(models.Model): From c903a925c4875bd1c5642b8e9982ba3bffa10728 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Mon, 4 Nov 2024 09:38:44 +0200 Subject: [PATCH 28/46] Moving common journey mapper function in common function --- plugins/wfp/common.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugins/wfp/common.py b/plugins/wfp/common.py index 3871e1e00b..28cda675c9 100644 --- a/plugins/wfp/common.py +++ b/plugins/wfp/common.py @@ -16,6 +16,11 @@ def delete_beneficiaries(self): print("EXISTING VISITS DELETED", beneficiary[1]["wfp.Visit"]) print("EXISTING JOURNEY DELETED", beneficiary[1]["wfp.Journey"]) + def account_related_to_entity_type(self): + entity_type = EntityType.objects.get(code=self.type) + account = Account.objects.get(id=entity_type.account_id) + return account + def retrieve_entities(self): steps_id = ETL().steps_to_exclude() updated_at = date(2023, 7, 10) @@ -536,3 +541,19 @@ def calculate_birth_date(self, current_record): elif age_entry == "months": calculated_date = registered_at - relativedelta(months=beneficiary_age) return calculated_date + + def entity_journey_mapper(self, visits, anthropometric_visit_forms, admission_form, current_journey): + journey = [] + for index, visit in enumerate(visits): + if visit: + current_journey["weight_gain"] = visit.get("weight_gain", None) + current_journey["weight_loss"] = visit.get("weight_loss", None) + if visit.get("duration", None) is not None and visit.get("duration", None) != "": + current_journey["duration"] = visit.get("duration") + + current_journey = ETL().journey_Formatter( + visit, admission_form, anthropometric_visit_forms, current_journey, visits, index + ) + current_journey["steps"].append(visit) + journey.append(current_journey) + return journey From 14c97ccb5724c5bbdaa271575511c6c7422af843 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Tue, 5 Nov 2024 09:35:40 +0200 Subject: [PATCH 29/46] Allow to filter on account journey/visit --- plugins/wfp/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/wfp/admin.py b/plugins/wfp/admin.py index 12fe8edbb5..a7ca280981 100644 --- a/plugins/wfp/admin.py +++ b/plugins/wfp/admin.py @@ -31,6 +31,7 @@ class JourneyAdmin(admin.ModelAdmin): "weight_loss", "exit_type", "instance_id", + "beneficiary", ) list_filter = ( "admission_criteria", @@ -40,6 +41,7 @@ class JourneyAdmin(admin.ModelAdmin): "start_date", "end_date", "exit_type", + "beneficiary__account", ) @@ -47,7 +49,7 @@ class JourneyAdmin(admin.ModelAdmin): class VisitAdmin(admin.ModelAdmin): list_display = ("id", "date", "number", "org_unit", "journey") raw_id_fields = ("org_unit", "journey") - list_filter = ("date", "number", "journey__programme_type") + list_filter = ("date", "number", "journey__programme_type", "journey__beneficiary__account") @admin.register(Step) From c69396015e167704a5cec3eb4239bfb2ee666099 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Tue, 5 Nov 2024 09:41:57 +0200 Subject: [PATCH 30/46] Fix program linked to beneficiary --- plugins/wfp/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/wfp/common.py b/plugins/wfp/common.py index 28cda675c9..b049b28ed8 100644 --- a/plugins/wfp/common.py +++ b/plugins/wfp/common.py @@ -97,6 +97,8 @@ def program_mapper(self, visit): program = visit.get("program") else: program = "" + else: + program = visit.get("program") elif visit.get("program_two") is not None and visit.get("program_two") != "NONE": program = visit.get("program_two", None) elif visit.get("discharge_program") is not None and visit.get("discharge_program") != "NONE": From 6781ada86a128f3e34df4a377cf62ba18b90b45f Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Tue, 5 Nov 2024 09:55:04 +0200 Subject: [PATCH 31/46] WIP:Splitting wfp plugin into 2 plugins to separate South Sudan and Nigeria account --- hat/settings.py | 2 + .../wfp/nigeria/management/commands/Under5.py | 230 ++++++++++++++++++ .../management/commands/etl.py} | 2 +- .../south_sudan/management/commands/etl.py | 9 + .../management/commands/etl_Under5.py} | 10 +- .../management/commands/etl_pbwg.py} | 10 +- .../management/commands/wfp_random_data.py | 4 +- plugins/wfp/tasks.py | 7 +- 8 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 plugins/wfp/nigeria/management/commands/Under5.py rename plugins/wfp/{management/commands/wfp_etl.py => nigeria/management/commands/etl.py} (89%) create mode 100644 plugins/wfp/south_sudan/management/commands/etl.py rename plugins/wfp/{management/commands/wfp_etl_Under5.py => south_sudan/management/commands/etl_Under5.py} (98%) rename plugins/wfp/{management/commands/wfp_etl_pbwg.py => south_sudan/management/commands/etl_pbwg.py} (97%) rename plugins/wfp/{ => south_sudan}/management/commands/wfp_random_data.py (72%) diff --git a/hat/settings.py b/hat/settings.py index 97e051be6d..4d3c846f4c 100644 --- a/hat/settings.py +++ b/hat/settings.py @@ -177,6 +177,8 @@ "allauth.account", "allauth.socialaccount", "storages", + "plugins.wfp.south_sudan", + "plugins.wfp.nigeria", ] if ENABLE_CORS: INSTALLED_APPS += [ diff --git a/plugins/wfp/nigeria/management/commands/Under5.py b/plugins/wfp/nigeria/management/commands/Under5.py new file mode 100644 index 0000000000..d4b9dc4d35 --- /dev/null +++ b/plugins/wfp/nigeria/management/commands/Under5.py @@ -0,0 +1,230 @@ +from ....models import * +from iaso.models import * +from django.core.management.base import BaseCommand +from itertools import groupby +from operator import itemgetter +from ....common import ETL +import logging + +logger = logging.getLogger(__name__) + + +class NG_Under5: + def compute_gained_weight(self, initial_weight, current_weight, duration): + weight_gain = 0 + weight_loss = 0 + + weight_difference = 0 + if initial_weight is not None and current_weight is not None and current_weight != "": + initial_weight = float(initial_weight) + current_weight = float(current_weight) + weight_difference = round(((current_weight * 1000) - (initial_weight * 1000)), 4) + if weight_difference >= 0: + if duration == 0: + weight_gain = 0 + elif duration > 0 and current_weight > 0 and initial_weight > 0: + weight_gain = round((weight_difference / (initial_weight * float(duration))), 4) + elif weight_difference < 0: + weight_loss = abs(weight_difference) + return { + "initial_weight": float(initial_weight) if initial_weight is not None else initial_weight, + "discharge_weight": ( + float(current_weight) if current_weight is not None and current_weight != "" else current_weight + ), + "weight_difference": weight_difference, + "weight_gain": weight_gain, + "weight_loss": weight_loss / 1000, + } + + def group_visit_by_entity(self, entities): + instances = [] + i = 0 + instances_by_entity = groupby(list(entities), key=itemgetter("entity_id")) + initial_weight = None + current_weight = None + initial_date = None + current_date = None + duration = 0 + for entity_id, entity in instances_by_entity: + instances.append({"entity_id": entity_id, "visits": [], "journey": []}) + for visit in entity: + current_record = visit.get("json", None) + instances[i]["program"] = ETL().program_mapper(current_record) + if current_record is not None and current_record != None: + if ( + current_record.get("actual_birthday__date__") is not None + and current_record.get("actual_birthday__date__", None) != "" + ): + birth_date = current_record.get("actual_birthday__date__", None) + instances[i]["birth_date"] = birth_date[:10] + elif ( + current_record.get("age_entry", None) is not None + and current_record.get("age_entry", None) != "" + ): + calculated_date = ETL().calculate_birth_date(current_record) + instances[i]["birth_date"] = calculated_date + if current_record.get("gender") is not None: + gender = current_record.get("gender", "") + if current_record.get("gender") == "F": + gender = "Female" + elif current_record.get("gender") == "M": + gender = "Male" + instances[i]["gender"] = gender + if current_record.get("last_name") is not None: + instances[i]["last_name"] = current_record.get("last_name", "") + + if current_record.get("first_name") is not None: + instances[i]["first_name"] = current_record.get("first_name", "") + + form_id = visit.get("form__form_id") + current_record["org_unit_id"] = visit.get("org_unit_id", None) + + if current_record.get("weight_kgs", None) is not None: + current_weight = current_record.get("weight_kgs", None) + print("CURRENT WEIGHT ...:", current_weight) + elif current_record.get("previous_weight_kgs__decimal__", None) is not None: + current_weight = current_record.get("previous_weight_kgs__decimal__", None) + current_date = visit.get( + "source_created_at", + visit.get( + "_visit_date", visit.get("visit_date", visit.get("_new_discharged_today", current_date)) + ), + ) + + if form_id == "Anthropometric visit child": + initial_weight = current_weight + instances[i]["initial_weight"] = initial_weight + visit_date = visit.get( + "source_created_at", visit.get("_visit_date", visit.get("visit_date", current_date)) + ) + initial_date = visit_date + + if initial_date is not None: + duration = (current_date - initial_date).days + current_record["start_date"] = initial_date.strftime("%Y-%m-%d") + + weight = self.compute_gained_weight(initial_weight, current_weight, duration) + current_record["end_date"] = current_date.strftime("%Y-%m-%d") + current_record["weight_gain"] = weight["weight_gain"] + current_record["weight_loss"] = weight["weight_loss"] + current_record["initial_weight"] = weight["initial_weight"] + current_record["discharge_weight"] = weight["discharge_weight"] + current_record["weight_difference"] = weight["weight_difference"] + current_record["duration"] = duration + + visit_date = visit.get( + "source_created_at", visit.get("_visit_date", visit.get("visit_date", current_date)) + ) + if visit_date: + current_record["date"] = visit_date.strftime("%Y-%m-%d") + + current_record["instance_id"] = visit["id"] + current_record["form_id"] = form_id + instances[i]["visits"].append(current_record) + i = i + 1 + return list( + filter( + lambda instance: ( + instance.get("visits") + and len(instance.get("visits")) > 1 + and instance.get("gender") is not None + and instance.get("gender") != "" + and instance.get("birth_date") is not None + and instance.get("birth_date") != "" + ), + instances, + ) + ) + + def run(self): + entity_type = ETL("ng_-_tsfp_child_3") + type = EntityType.objects.get(code="ng_-_tsfp_child_3") + account = entity_type.account_related_to_entity_type() + beneficiaries = entity_type.retrieve_entities() + print("CURRENT ACCOUNT ", account) + logger.info(f"Instances linked to Child Under 5 program: {beneficiaries.count()} for {type.name} on {account}") + entities = sorted(list(beneficiaries), key=itemgetter("entity_id")) + existing_beneficiaries = ETL().existing_beneficiaries() + instances = self.group_visit_by_entity(entities) + + for index, instance in enumerate(instances): + logger.info( + f"---------------------------------------- Beneficiary N° {(index+1)} {instance['entity_id']}-----------------------------------" + ) + instance["journey"] = self.journeyMapper(instance["visits"], "Anthropometric visit child") + beneficiary = Beneficiary() + if instance["entity_id"] not in existing_beneficiaries and len(instance["journey"][0]["visits"]) > 0: + beneficiary.gender = instance["gender"] + beneficiary.birth_date = instance["birth_date"] + beneficiary.entity_id = instance["entity_id"] + beneficiary.account = account + beneficiary.save() + logger.info(f"Created new beneficiary") + else: + beneficiary = Beneficiary.objects.filter(entity_id=instance["entity_id"]).first() + + logger.info("Retrieving journey linked to beneficiary") + + for journey_instance in instance["journey"]: + if len(journey_instance["visits"]) > 0: + journey = self.save_journey(beneficiary, journey_instance) + visits = ETL().save_visit(journey_instance["visits"], journey) + logger.info(f"Inserted {len(visits)} Visits") + grouped_steps = ETL().get_admission_steps(journey_instance["steps"]) + admission_step = grouped_steps[0] + + followUpVisits = ETL().group_followup_steps(grouped_steps, admission_step) + + steps = ETL().save_steps(visits, followUpVisits) + logger.info(f"Inserted {len(steps)} Steps") + else: + logger.info("No new journey") + logger.info( + f"---------------------------------------------------------------------------------------------\n\n" + ) + + def journeyMapper(self, visits, admission_form): + current_journey = {"visits": [], "steps": []} + anthropometric_visit_forms = [ + "anthropometric_second_visit_tsfp", + "anthropometric_second_visit_otp", + ] + # admission_form = "Anthropometric visit child" + print("ALL VISIT ...:", visits) + visit_nutrition_program = [visit for visit in visits if visit["form_id"] == admission_form] + if len(visit_nutrition_program) > 0: + nutrition_programme = ETL().program_mapper(visit_nutrition_program[0]) + if nutrition_programme == "TSFP-MAM": + current_journey["nutrition_programme"] = "TSFP" + elif nutrition_programme == "OTP-MAM": + current_journey["nutrition_programme"] = "OTP" + else: + current_journey["nutrition_programme"] = nutrition_programme + journey = ETL().entity_journey_mapper(visits, anthropometric_visit_forms, admission_form, current_journey) + return journey + + def save_journey(self, beneficiary, record): + journey = Journey() + journey.beneficiary = beneficiary + journey.programme_type = "U5" + journey.admission_criteria = record["admission_criteria"] + journey.admission_type = record.get("admission_type", None) + journey.nutrition_programme = record["nutrition_programme"] + journey.exit_type = record.get("exit_type", None) + journey.instance_id = record.get("instance_id", None) + journey.initial_weight = record.get("initial_weight", None) + journey.start_date = record.get("start_date", None) + journey.duration = record.get("duration", None) + journey.end_date = record.get("end_date", None) + + # Calculate the weight gain only for cured and Transfer from OTP to TSFP cases! + if ( + record.get("exit_type", None) is not None + and record.get("exit_type", None) != "" + and record.get("exit_type", None) in ["cured", "transfer_to_tsfp"] + ): + journey.discharge_weight = record.get("discharge_weight", None) + journey.weight_gain = record.get("weight_gain", 0) + journey.weight_loss = record.get("weight_loss", 0) + journey.save() + return journey diff --git a/plugins/wfp/management/commands/wfp_etl.py b/plugins/wfp/nigeria/management/commands/etl.py similarity index 89% rename from plugins/wfp/management/commands/wfp_etl.py rename to plugins/wfp/nigeria/management/commands/etl.py index ea81d48cd1..765ebfdd93 100644 --- a/plugins/wfp/management/commands/wfp_etl.py +++ b/plugins/wfp/nigeria/management/commands/etl.py @@ -1,5 +1,5 @@ from django.core.management.base import BaseCommand -from ...tasks import etl +from ....tasks import etl class Command(BaseCommand): diff --git a/plugins/wfp/south_sudan/management/commands/etl.py b/plugins/wfp/south_sudan/management/commands/etl.py new file mode 100644 index 0000000000..765ebfdd93 --- /dev/null +++ b/plugins/wfp/south_sudan/management/commands/etl.py @@ -0,0 +1,9 @@ +from django.core.management.base import BaseCommand +from ....tasks import etl + + +class Command(BaseCommand): + help = "Transform WFP collected data in a format usable for analytics" + + def handle(self, *args, **options): + etl() diff --git a/plugins/wfp/management/commands/wfp_etl_Under5.py b/plugins/wfp/south_sudan/management/commands/etl_Under5.py similarity index 98% rename from plugins/wfp/management/commands/wfp_etl_Under5.py rename to plugins/wfp/south_sudan/management/commands/etl_Under5.py index 0899477e94..01e2e730f7 100644 --- a/plugins/wfp/management/commands/wfp_etl_Under5.py +++ b/plugins/wfp/south_sudan/management/commands/etl_Under5.py @@ -1,8 +1,8 @@ -from ...models import * +from ....models import * from django.core.management.base import BaseCommand from itertools import groupby from operator import itemgetter -from ...common import ETL +from ....common import ETL import logging logger = logging.getLogger(__name__) @@ -134,13 +134,13 @@ def group_visit_by_entity(self, entities): ) ) - def journeyMapper(self, visits): + def journeyMapper(self, visits, admission_form): current_journey = {"visits": [], "steps": []} anthropometric_visit_forms = [ "child_antropometric_followUp_tsfp", "child_antropometric_followUp_otp", ] - admission_form = "Anthropometric visit child" + # admission_form = "Anthropometric visit child" visit_nutrition_program = [visit for visit in visits if visit["form_id"] == admission_form] if len(visit_nutrition_program) > 0: current_journey["nutrition_programme"] = ETL().program_mapper(visit_nutrition_program[0]) @@ -186,7 +186,7 @@ def run(self): logger.info( f"---------------------------------------- Beneficiary N° {(index+1)} {instance['entity_id']}-----------------------------------" ) - instance["journey"] = self.journeyMapper(instance["visits"]) + instance["journey"] = self.journeyMapper(instance["visits"], "Anthropometric visit child") beneficiary = Beneficiary() if instance["entity_id"] not in existing_beneficiaries and len(instance["journey"][0]["visits"]) > 0: beneficiary.gender = instance["gender"] diff --git a/plugins/wfp/management/commands/wfp_etl_pbwg.py b/plugins/wfp/south_sudan/management/commands/etl_pbwg.py similarity index 97% rename from plugins/wfp/management/commands/wfp_etl_pbwg.py rename to plugins/wfp/south_sudan/management/commands/etl_pbwg.py index f248ac8dee..1d59cb9ec8 100644 --- a/plugins/wfp/management/commands/wfp_etl_pbwg.py +++ b/plugins/wfp/south_sudan/management/commands/etl_pbwg.py @@ -1,8 +1,8 @@ -from ...models import * +from ....models import * from django.core.management.base import BaseCommand from itertools import groupby from operator import itemgetter -from ...common import ETL +from ....common import ETL import logging logger = logging.getLogger(__name__) @@ -22,7 +22,7 @@ def run(self): logger.info( f"---------------------------------------- Beneficiary N° {(index+1)} {instance['entity_id']}-----------------------------------" ) - instance["journey"] = self.journeyMapper(instance["visits"]) + instance["journey"] = self.journeyMapper(instance["visits"], "wfp_coda_pbwg_anthropometric") beneficiary = Beneficiary() if instance["entity_id"] not in existing_beneficiaries and len(instance["journey"][0]["visits"]) > 0: beneficiary.gender = "" @@ -74,13 +74,13 @@ def save_journey(self, beneficiary, record): return journey - def journeyMapper(self, visits): + def journeyMapper(self, visits, admission_form): current_journey = {"visits": [], "steps": []} anthropometric_visit_forms = [ "wfp_coda_pbwg_luctating_followup_anthro", "wfp_coda_pbwg_followup_anthro", ] - admission_form = "wfp_coda_pbwg_anthropometric" + # admission_form = "wfp_coda_pbwg_anthropometric" visit_nutrition_program = [visit for visit in visits if visit["form_id"] == "wfp_coda_pbwg_registration"][0] if len(visit_nutrition_program) > 0: current_journey["nutrition_programme"] = visit_nutrition_program.get("physiology_status", None) diff --git a/plugins/wfp/management/commands/wfp_random_data.py b/plugins/wfp/south_sudan/management/commands/wfp_random_data.py similarity index 72% rename from plugins/wfp/management/commands/wfp_random_data.py rename to plugins/wfp/south_sudan/management/commands/wfp_random_data.py index 24c51e2a2b..6def46b706 100644 --- a/plugins/wfp/management/commands/wfp_random_data.py +++ b/plugins/wfp/south_sudan/management/commands/wfp_random_data.py @@ -1,7 +1,7 @@ -from ...models import * # type: ignore +from ....models import * # type: ignore from django.core.management.base import BaseCommand -from ...tasks import generate_random_data +from ....tasks import generate_random_data class Command(BaseCommand): diff --git a/plugins/wfp/tasks.py b/plugins/wfp/tasks.py index 27179696af..d1c5a83f35 100644 --- a/plugins/wfp/tasks.py +++ b/plugins/wfp/tasks.py @@ -4,10 +4,10 @@ import random from celery import shared_task import datetime -from .management.commands.wfp_etl_Under5 import Under5 -from .management.commands.wfp_etl_pbwg import PBWG +from .south_sudan.management.commands.etl_Under5 import Under5 +from .south_sudan.management.commands.etl_pbwg import PBWG +from .nigeria.management.commands.Under5 import NG_Under5 import logging -from .common import ETL logger = logging.getLogger(__name__) @@ -85,3 +85,4 @@ def etl(): # Before copying beneficiary data, clean all wfp table and reimport data! Under5().run() PBWG().run() + NG_Under5().run() From 8d109c9c6064cf8386b131a4e1397a4870f7a0b2 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Tue, 5 Nov 2024 09:56:27 +0200 Subject: [PATCH 32/46] Fix description to explain how to launch ETL script --- plugins/wfp/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wfp/README.md b/plugins/wfp/README.md index 6551ba6755..b22606f3ea 100644 --- a/plugins/wfp/README.md +++ b/plugins/wfp/README.md @@ -16,8 +16,8 @@ Currently, it reads and writes to the same database as the Coda2 installation. Both have default value of `redis://localhost:6379` -3. Run `python manage.py migrate` to create the wfp specific database tables. -4. To test if the setup is correct outside of celery, run `python manage.py wfp_etl` +3. Run `docker compose run iaso manage migrate wfp` to create the wfp specific database tables. +4. To test if the setup is correct outside of celery, run `docker compose run iaso manage etl` 5. The name of the module to run by celery is `hat`: `python -m celery -A hat worker -l info` should give you the list of available tasks. 6. You now have two celery task that can be triggered: From 5acb2fdd6c5f30d35f745ce7b9ff1e20318c7975 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Wed, 6 Nov 2024 13:48:53 +0200 Subject: [PATCH 33/46] Adding account filter into step table --- plugins/wfp/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wfp/admin.py b/plugins/wfp/admin.py index a7ca280981..81e87b6690 100644 --- a/plugins/wfp/admin.py +++ b/plugins/wfp/admin.py @@ -54,5 +54,5 @@ class VisitAdmin(admin.ModelAdmin): @admin.register(Step) class StepAdmin(admin.ModelAdmin): - list_display = ("id", "assistance_type", "visit") - list_filter = ("assistance_type", "visit__journey__programme_type") + list_display = ("id", "assistance_type", "quantity_given", "visit") + list_filter = ("assistance_type", "visit__journey__programme_type", "visit__journey__beneficiary__account") From 20b71dd6c30a90894e8816b74d05663afe9a6eec Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Wed, 6 Nov 2024 14:07:38 +0200 Subject: [PATCH 34/46] Move weight gain calculation to common file --- plugins/wfp/common.py | 72 +++++++++++++++++-- .../wfp/nigeria/management/commands/Under5.py | 55 ++++---------- 2 files changed, 79 insertions(+), 48 deletions(-) diff --git a/plugins/wfp/common.py b/plugins/wfp/common.py index b049b28ed8..7bc348e0b3 100644 --- a/plugins/wfp/common.py +++ b/plugins/wfp/common.py @@ -17,15 +17,15 @@ def delete_beneficiaries(self): print("EXISTING JOURNEY DELETED", beneficiary[1]["wfp.Journey"]) def account_related_to_entity_type(self): - entity_type = EntityType.objects.get(code=self.type) - account = Account.objects.get(id=entity_type.account_id) + entity_type = EntityType.objects.filter(code__in=self.type) + account = Account.objects.get(id=entity_type[0].account_id) return account def retrieve_entities(self): steps_id = ETL().steps_to_exclude() updated_at = date(2023, 7, 10) beneficiaries = ( - Instance.objects.filter(entity__entity_type__code=self.type) + Instance.objects.filter(entity__entity_type__code__in=self.type) # .filter(entity__id__in=[1, 42, 46, 49, 58, 77, 90, 111, 322, 323, 330, 196, 226, 254,315, 424, 430, 431, 408, 19, 230, 359]) # .filter(entity__id__in=[230, 359, 254]) .filter(json__isnull=False) @@ -309,7 +309,9 @@ def exit_by_defaulter(self, visits, visit, anthropometric_visit_forms): def journey_Formatter(self, visit, anthropometric_visit_form, followup_forms, current_journey, visits, index): default_anthropometric_followup_forms = followup_forms - if visit["form_id"] == anthropometric_visit_form: + default_admission_form = None + if visit["form_id"] in anthropometric_visit_form: + default_admission_form = visit["form_id"] current_journey["instance_id"] = visit.get("instance_id", None) current_journey["start_date"] = visit.get("start_date", None) current_journey["initial_weight"] = visit.get("initial_weight", None) @@ -323,7 +325,7 @@ def journey_Formatter(self, visit, anthropometric_visit_form, followup_forms, cu current_journey["programme_type"] = self.program_mapper(visit) current_journey["org_unit_id"] = visit.get("org_unit_id") current_journey["visits"].append(visit) - followup_forms.append(anthropometric_visit_form) + followup_forms.append(default_admission_form) exit = None if visit["form_id"] in followup_forms: @@ -376,7 +378,6 @@ def split_given_medication(self, medication, quantity): def map_assistance_step(self, step, given_assistance): quantity = 1 - if (step.get("net_given") is not None and step.get("net_given") == "yes") or ( step.get("net_given__bool__") is not None and step.get("net_given__bool__") == "1" ): @@ -409,6 +410,26 @@ def map_assistance_step(self, step, given_assistance): given_medication = self.split_given_medication(step.get("medication_2"), quantity) given_assistance = given_assistance + given_medication + if step.get("vitamins_given", None) is not None and step.get("vitamins_given", None) == "1": + assistance = {"type": "Vitamin", "quantity": quantity} + given_assistance.append(assistance) + + if step.get("ab_given", None) is not None and step.get("ab_given", None) == "1": + assistance = {"type": "albendazole", "quantity": quantity} + given_assistance.append(assistance) + + if step.get("measles_vacc", None) is not None and step.get("measles_vacc", None) == "1": + assistance = {"type": "Measles vaccination", "quantity": quantity} + given_assistance.append(assistance) + + if step.get("art_given", None) is not None and step.get("art_given", None) == "1": + assistance = {"type": "ART", "quantity": quantity} + given_assistance.append(assistance) + + if step.get("anti_helminth_given", None) is not None and step.get("anti_helminth_given", None) != "": + assistance = {"type": step.get("anti_helminth_given"), "quantity": quantity} + given_assistance.append(assistance) + if step.get("ration_to_distribute") is not None or step.get("ration") is not None: quantity = 0 ration_type = "" @@ -442,6 +463,19 @@ def map_assistance_step(self, step, given_assistance): "quantity": quantity, } given_assistance.append(assistance) + elif step.get("ration_type") is not None: + if step.get("ration_type") in ["csb", "csb1", "csb2"]: + quantity = step.get("_csb_packets") + elif step.get("ration_type") == "lndf": + quantity = step.get("_lndf_kgs") + else: + quantity = step.get("_total_number_of_sachets", None) + assistance = { + "type": step.get("ration_type"), + "quantity": quantity, + } + given_assistance.append(assistance) + return list( filter( lambda assistance: (assistance.get("type") and assistance.get("type") != ""), @@ -559,3 +593,29 @@ def entity_journey_mapper(self, visits, anthropometric_visit_forms, admission_fo current_journey["steps"].append(visit) journey.append(current_journey) return journey + + def compute_gained_weight(self, initial_weight, current_weight, duration): + weight_gain = 0 + weight_loss = 0 + + weight_difference = 0 + if initial_weight is not None and current_weight is not None and current_weight != "": + initial_weight = float(initial_weight) + current_weight = float(current_weight) + weight_difference = round(((current_weight * 1000) - (initial_weight * 1000)), 4) + if weight_difference >= 0: + if duration == 0: + weight_gain = 0 + elif duration > 0 and current_weight > 0 and initial_weight > 0: + weight_gain = round((weight_difference / (initial_weight * float(duration))), 4) + elif weight_difference < 0: + weight_loss = abs(weight_difference) + return { + "initial_weight": float(initial_weight) if initial_weight is not None else initial_weight, + "discharge_weight": ( + float(current_weight) if current_weight is not None and current_weight != "" else current_weight + ), + "weight_difference": weight_difference, + "weight_gain": weight_gain, + "weight_loss": weight_loss / 1000, + } diff --git a/plugins/wfp/nigeria/management/commands/Under5.py b/plugins/wfp/nigeria/management/commands/Under5.py index d4b9dc4d35..f3e5735097 100644 --- a/plugins/wfp/nigeria/management/commands/Under5.py +++ b/plugins/wfp/nigeria/management/commands/Under5.py @@ -10,32 +10,6 @@ class NG_Under5: - def compute_gained_weight(self, initial_weight, current_weight, duration): - weight_gain = 0 - weight_loss = 0 - - weight_difference = 0 - if initial_weight is not None and current_weight is not None and current_weight != "": - initial_weight = float(initial_weight) - current_weight = float(current_weight) - weight_difference = round(((current_weight * 1000) - (initial_weight * 1000)), 4) - if weight_difference >= 0: - if duration == 0: - weight_gain = 0 - elif duration > 0 and current_weight > 0 and initial_weight > 0: - weight_gain = round((weight_difference / (initial_weight * float(duration))), 4) - elif weight_difference < 0: - weight_loss = abs(weight_difference) - return { - "initial_weight": float(initial_weight) if initial_weight is not None else initial_weight, - "discharge_weight": ( - float(current_weight) if current_weight is not None and current_weight != "" else current_weight - ), - "weight_difference": weight_difference, - "weight_gain": weight_gain, - "weight_loss": weight_loss / 1000, - } - def group_visit_by_entity(self, entities): instances = [] i = 0 @@ -81,7 +55,6 @@ def group_visit_by_entity(self, entities): if current_record.get("weight_kgs", None) is not None: current_weight = current_record.get("weight_kgs", None) - print("CURRENT WEIGHT ...:", current_weight) elif current_record.get("previous_weight_kgs__decimal__", None) is not None: current_weight = current_record.get("previous_weight_kgs__decimal__", None) current_date = visit.get( @@ -91,7 +64,7 @@ def group_visit_by_entity(self, entities): ), ) - if form_id == "Anthropometric visit child": + if form_id in ["Anthropometric visit child", "anthropometric_admission_otp"]: initial_weight = current_weight instances[i]["initial_weight"] = initial_weight visit_date = visit.get( @@ -103,7 +76,7 @@ def group_visit_by_entity(self, entities): duration = (current_date - initial_date).days current_record["start_date"] = initial_date.strftime("%Y-%m-%d") - weight = self.compute_gained_weight(initial_weight, current_weight, duration) + weight = ETL().compute_gained_weight(initial_weight, current_weight, duration) current_record["end_date"] = current_date.strftime("%Y-%m-%d") current_record["weight_gain"] = weight["weight_gain"] current_record["weight_loss"] = weight["weight_loss"] @@ -137,11 +110,12 @@ def group_visit_by_entity(self, entities): ) def run(self): - entity_type = ETL("ng_-_tsfp_child_3") + children_type = ["ng_-_tsfp_child_3", "ng_-_otp_child_3"] + entity_type = ETL(children_type) type = EntityType.objects.get(code="ng_-_tsfp_child_3") account = entity_type.account_related_to_entity_type() beneficiaries = entity_type.retrieve_entities() - print("CURRENT ACCOUNT ", account) + logger.info(f"Instances linked to Child Under 5 program: {beneficiaries.count()} for {type.name} on {account}") entities = sorted(list(beneficiaries), key=itemgetter("entity_id")) existing_beneficiaries = ETL().existing_beneficiaries() @@ -151,7 +125,9 @@ def run(self): logger.info( f"---------------------------------------- Beneficiary N° {(index+1)} {instance['entity_id']}-----------------------------------" ) - instance["journey"] = self.journeyMapper(instance["visits"], "Anthropometric visit child") + instance["journey"] = self.journeyMapper( + instance["visits"], ["Anthropometric visit child", "anthropometric_admission_otp"] + ) beneficiary = Beneficiary() if instance["entity_id"] not in existing_beneficiaries and len(instance["journey"][0]["visits"]) > 0: beneficiary.gender = instance["gender"] @@ -189,14 +165,13 @@ def journeyMapper(self, visits, admission_form): "anthropometric_second_visit_tsfp", "anthropometric_second_visit_otp", ] - # admission_form = "Anthropometric visit child" - print("ALL VISIT ...:", visits) - visit_nutrition_program = [visit for visit in visits if visit["form_id"] == admission_form] + visit_nutrition_program = [visit for visit in visits if visit["form_id"] in admission_form] + if len(visit_nutrition_program) > 0: nutrition_programme = ETL().program_mapper(visit_nutrition_program[0]) if nutrition_programme == "TSFP-MAM": current_journey["nutrition_programme"] = "TSFP" - elif nutrition_programme == "OTP-MAM": + elif nutrition_programme == "OTP-SAM": current_journey["nutrition_programme"] = "OTP" else: current_journey["nutrition_programme"] = nutrition_programme @@ -217,12 +192,8 @@ def save_journey(self, beneficiary, record): journey.duration = record.get("duration", None) journey.end_date = record.get("end_date", None) - # Calculate the weight gain only for cured and Transfer from OTP to TSFP cases! - if ( - record.get("exit_type", None) is not None - and record.get("exit_type", None) != "" - and record.get("exit_type", None) in ["cured", "transfer_to_tsfp"] - ): + # Calculate the weight gain only for exited cases! + if record.get("exit_type", None) is not None and record.get("exit_type", None) != "": journey.discharge_weight = record.get("discharge_weight", None) journey.weight_gain = record.get("weight_gain", 0) journey.weight_loss = record.get("weight_loss", 0) From dc92f73ebe253f9721fea446a769d218d2c44544 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Wed, 6 Nov 2024 14:17:07 +0200 Subject: [PATCH 35/46] Separating ETL task runner for South Sudan and Nigeria --- plugins/wfp/README.md | 2 +- .../management/commands/{etl.py => etl_ng.py} | 4 +-- .../commands/{etl_pbwg.py => Pbwg.py} | 4 +-- .../commands/{etl_Under5.py => Under5.py} | 35 +++---------------- .../commands/{etl.py => etl_ssd.py} | 4 +-- plugins/wfp/tasks.py | 16 +++++---- 6 files changed, 21 insertions(+), 44 deletions(-) rename plugins/wfp/nigeria/management/commands/{etl.py => etl_ng.py} (81%) rename plugins/wfp/south_sudan/management/commands/{etl_pbwg.py => Pbwg.py} (98%) rename plugins/wfp/south_sudan/management/commands/{etl_Under5.py => Under5.py} (86%) rename plugins/wfp/south_sudan/management/commands/{etl.py => etl_ssd.py} (80%) diff --git a/plugins/wfp/README.md b/plugins/wfp/README.md index b22606f3ea..5a39b6dedc 100644 --- a/plugins/wfp/README.md +++ b/plugins/wfp/README.md @@ -17,7 +17,7 @@ Currently, it reads and writes to the same database as the Coda2 installation. Both have default value of `redis://localhost:6379` 3. Run `docker compose run iaso manage migrate wfp` to create the wfp specific database tables. -4. To test if the setup is correct outside of celery, run `docker compose run iaso manage etl` +4. To test if the setup is correct outside of celery, run `docker compose run iaso manage etl_ssd` 5. The name of the module to run by celery is `hat`: `python -m celery -A hat worker -l info` should give you the list of available tasks. 6. You now have two celery task that can be triggered: diff --git a/plugins/wfp/nigeria/management/commands/etl.py b/plugins/wfp/nigeria/management/commands/etl_ng.py similarity index 81% rename from plugins/wfp/nigeria/management/commands/etl.py rename to plugins/wfp/nigeria/management/commands/etl_ng.py index 765ebfdd93..1d7d4b5761 100644 --- a/plugins/wfp/nigeria/management/commands/etl.py +++ b/plugins/wfp/nigeria/management/commands/etl_ng.py @@ -1,9 +1,9 @@ from django.core.management.base import BaseCommand -from ....tasks import etl +from ....tasks import etl_ng class Command(BaseCommand): help = "Transform WFP collected data in a format usable for analytics" def handle(self, *args, **options): - etl() + etl_ng() diff --git a/plugins/wfp/south_sudan/management/commands/etl_pbwg.py b/plugins/wfp/south_sudan/management/commands/Pbwg.py similarity index 98% rename from plugins/wfp/south_sudan/management/commands/etl_pbwg.py rename to plugins/wfp/south_sudan/management/commands/Pbwg.py index 1d59cb9ec8..abf7fc4377 100644 --- a/plugins/wfp/south_sudan/management/commands/etl_pbwg.py +++ b/plugins/wfp/south_sudan/management/commands/Pbwg.py @@ -10,7 +10,7 @@ class PBWG: def run(self): - entity_type = ETL("pbwg_1") + entity_type = ETL(["pbwg_1"]) account = entity_type.account_related_to_entity_type() beneficiaries = entity_type.retrieve_entities() logger.info(f"Instances linked to PBWG program: {beneficiaries.count()}") @@ -22,7 +22,7 @@ def run(self): logger.info( f"---------------------------------------- Beneficiary N° {(index+1)} {instance['entity_id']}-----------------------------------" ) - instance["journey"] = self.journeyMapper(instance["visits"], "wfp_coda_pbwg_anthropometric") + instance["journey"] = self.journeyMapper(instance["visits"], ["wfp_coda_pbwg_anthropometric"]) beneficiary = Beneficiary() if instance["entity_id"] not in existing_beneficiaries and len(instance["journey"][0]["visits"]) > 0: beneficiary.gender = "" diff --git a/plugins/wfp/south_sudan/management/commands/etl_Under5.py b/plugins/wfp/south_sudan/management/commands/Under5.py similarity index 86% rename from plugins/wfp/south_sudan/management/commands/etl_Under5.py rename to plugins/wfp/south_sudan/management/commands/Under5.py index 01e2e730f7..07414bd377 100644 --- a/plugins/wfp/south_sudan/management/commands/etl_Under5.py +++ b/plugins/wfp/south_sudan/management/commands/Under5.py @@ -9,32 +9,6 @@ class Under5: - def compute_gained_weight(self, initial_weight, current_weight, duration): - weight_gain = 0 - weight_loss = 0 - - weight_difference = 0 - if initial_weight is not None and current_weight is not None and current_weight != "": - initial_weight = float(initial_weight) - current_weight = float(current_weight) - weight_difference = round(((current_weight * 1000) - (initial_weight * 1000)), 4) - if weight_difference >= 0: - if duration == 0: - weight_gain = 0 - elif duration > 0 and current_weight > 0 and initial_weight > 0: - weight_gain = round((weight_difference / (initial_weight * float(duration))), 4) - elif weight_difference < 0: - weight_loss = abs(weight_difference) - return { - "initial_weight": float(initial_weight) if initial_weight is not None else initial_weight, - "discharge_weight": ( - float(current_weight) if current_weight is not None and current_weight != "" else current_weight - ), - "weight_difference": weight_difference, - "weight_gain": weight_gain, - "weight_loss": weight_loss / 1000, - } - def group_visit_by_entity(self, entities): instances = [] i = 0 @@ -101,7 +75,7 @@ def group_visit_by_entity(self, entities): duration = (current_date - initial_date).days current_record["start_date"] = initial_date.strftime("%Y-%m-%d") - weight = self.compute_gained_weight(initial_weight, current_weight, duration) + weight = ETL().compute_gained_weight(initial_weight, current_weight, duration) current_record["end_date"] = current_date.strftime("%Y-%m-%d") current_record["weight_gain"] = weight["weight_gain"] current_record["weight_loss"] = weight["weight_loss"] @@ -141,7 +115,7 @@ def journeyMapper(self, visits, admission_form): "child_antropometric_followUp_otp", ] # admission_form = "Anthropometric visit child" - visit_nutrition_program = [visit for visit in visits if visit["form_id"] == admission_form] + visit_nutrition_program = [visit for visit in visits if visit["form_id"] in admission_form] if len(visit_nutrition_program) > 0: current_journey["nutrition_programme"] = ETL().program_mapper(visit_nutrition_program[0]) journey = ETL().entity_journey_mapper(visits, anthropometric_visit_forms, admission_form, current_journey) @@ -174,7 +148,7 @@ def save_journey(self, beneficiary, record): return journey def run(self): - entity_type = ETL("child_under_5_1") + entity_type = ETL(["child_under_5_1"]) account = entity_type.account_related_to_entity_type() beneficiaries = entity_type.retrieve_entities() logger.info(f"Instances linked to Child Under 5 program: {beneficiaries.count()}") @@ -186,7 +160,7 @@ def run(self): logger.info( f"---------------------------------------- Beneficiary N° {(index+1)} {instance['entity_id']}-----------------------------------" ) - instance["journey"] = self.journeyMapper(instance["visits"], "Anthropometric visit child") + instance["journey"] = self.journeyMapper(instance["visits"], ["Anthropometric visit child"]) beneficiary = Beneficiary() if instance["entity_id"] not in existing_beneficiaries and len(instance["journey"][0]["visits"]) > 0: beneficiary.gender = instance["gender"] @@ -209,7 +183,6 @@ def run(self): admission_step = grouped_steps[0] followUpVisits = ETL().group_followup_steps(grouped_steps, admission_step) - steps = ETL().save_steps(visits, followUpVisits) logger.info(f"Inserted {len(steps)} Steps") else: diff --git a/plugins/wfp/south_sudan/management/commands/etl.py b/plugins/wfp/south_sudan/management/commands/etl_ssd.py similarity index 80% rename from plugins/wfp/south_sudan/management/commands/etl.py rename to plugins/wfp/south_sudan/management/commands/etl_ssd.py index 765ebfdd93..3facfdeb7b 100644 --- a/plugins/wfp/south_sudan/management/commands/etl.py +++ b/plugins/wfp/south_sudan/management/commands/etl_ssd.py @@ -1,9 +1,9 @@ from django.core.management.base import BaseCommand -from ....tasks import etl +from ....tasks import etl_ssd class Command(BaseCommand): help = "Transform WFP collected data in a format usable for analytics" def handle(self, *args, **options): - etl() + etl_ssd() diff --git a/plugins/wfp/tasks.py b/plugins/wfp/tasks.py index d1c5a83f35..0e6aac8275 100644 --- a/plugins/wfp/tasks.py +++ b/plugins/wfp/tasks.py @@ -4,8 +4,8 @@ import random from celery import shared_task import datetime -from .south_sudan.management.commands.etl_Under5 import Under5 -from .south_sudan.management.commands.etl_pbwg import PBWG +from .south_sudan.management.commands.Under5 import Under5 +from .south_sudan.management.commands.Pbwg import PBWG from .nigeria.management.commands.Under5 import NG_Under5 import logging @@ -79,10 +79,14 @@ def generate_random_data(): @shared_task() -def etl(): +def etl_ng(): """Extract beneficiary data from Iaso tables and store them in the format expected by existing tableau dashboards""" - logger.info("Starting ETL") - # Before copying beneficiary data, clean all wfp table and reimport data! + logger.info("Starting ETL for Nigeria") + NG_Under5().run() + + +@shared_task() +def etl_ssd(): + logger.info("Starting ETL for South Sudan") Under5().run() PBWG().run() - NG_Under5().run() From d95dd9ff9087c713d418a11bc81a4d095ebf7196 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Wed, 6 Nov 2024 15:26:54 +0200 Subject: [PATCH 36/46] Refactoring creating entity journey --- plugins/wfp/common.py | 16 ++++++++++++++++ .../wfp/nigeria/management/commands/Under5.py | 13 +------------ .../wfp/south_sudan/management/commands/Pbwg.py | 14 ++------------ .../south_sudan/management/commands/Under5.py | 16 +++------------- 4 files changed, 22 insertions(+), 37 deletions(-) diff --git a/plugins/wfp/common.py b/plugins/wfp/common.py index 7bc348e0b3..79a04d034c 100644 --- a/plugins/wfp/common.py +++ b/plugins/wfp/common.py @@ -619,3 +619,19 @@ def compute_gained_weight(self, initial_weight, current_weight, duration): "weight_gain": weight_gain, "weight_loss": weight_loss / 1000, } + + def save_entity_journey(self, journey, beneficiary, record, entity_type): + journey.beneficiary = beneficiary + journey.programme_type = entity_type + journey.admission_criteria = record["admission_criteria"] + journey.admission_type = record.get("admission_type", None) + journey.nutrition_programme = record["nutrition_programme"] + journey.exit_type = record.get("exit_type", None) + journey.instance_id = record.get("instance_id", None) + journey.start_date = record.get("start_date", None) + journey.end_date = record.get("end_date", None) + journey.duration = record.get("duration", None) + + journey.save() + + return journey diff --git a/plugins/wfp/nigeria/management/commands/Under5.py b/plugins/wfp/nigeria/management/commands/Under5.py index f3e5735097..3833f8efa7 100644 --- a/plugins/wfp/nigeria/management/commands/Under5.py +++ b/plugins/wfp/nigeria/management/commands/Under5.py @@ -180,22 +180,11 @@ def journeyMapper(self, visits, admission_form): def save_journey(self, beneficiary, record): journey = Journey() - journey.beneficiary = beneficiary - journey.programme_type = "U5" - journey.admission_criteria = record["admission_criteria"] - journey.admission_type = record.get("admission_type", None) - journey.nutrition_programme = record["nutrition_programme"] - journey.exit_type = record.get("exit_type", None) - journey.instance_id = record.get("instance_id", None) journey.initial_weight = record.get("initial_weight", None) - journey.start_date = record.get("start_date", None) - journey.duration = record.get("duration", None) - journey.end_date = record.get("end_date", None) # Calculate the weight gain only for exited cases! if record.get("exit_type", None) is not None and record.get("exit_type", None) != "": journey.discharge_weight = record.get("discharge_weight", None) journey.weight_gain = record.get("weight_gain", 0) journey.weight_loss = record.get("weight_loss", 0) - journey.save() - return journey + return ETL().save_entity_journey(journey, beneficiary, record, "U5") diff --git a/plugins/wfp/south_sudan/management/commands/Pbwg.py b/plugins/wfp/south_sudan/management/commands/Pbwg.py index abf7fc4377..2536dbdc59 100644 --- a/plugins/wfp/south_sudan/management/commands/Pbwg.py +++ b/plugins/wfp/south_sudan/management/commands/Pbwg.py @@ -57,22 +57,12 @@ def run(self): def save_journey(self, beneficiary, record): journey = Journey() - journey.beneficiary = beneficiary - journey.programme_type = "PLW" - journey.admission_criteria = record.get("admission_criteria", None) - journey.admission_type = record.get("admission_type", None) - journey.nutrition_programme = record.get("nutrition_programme", None) - journey.exit_type = record.get("exit_type", None) - journey.instance_id = record.get("instance_id", None) - journey.start_date = record.get("start_date", None) - journey.end_date = record.get("end_date", None) if record.get("exit_type", None) is not None and record.get("exit_type", None) != "": journey.duration = record.get("duration", None) journey.end_date = record.get("end_date", None) - journey.save() - return journey + return ETL().save_entity_journey(journey, beneficiary, record, "PLW") def journeyMapper(self, visits, admission_form): current_journey = {"visits": [], "steps": []} @@ -80,7 +70,7 @@ def journeyMapper(self, visits, admission_form): "wfp_coda_pbwg_luctating_followup_anthro", "wfp_coda_pbwg_followup_anthro", ] - # admission_form = "wfp_coda_pbwg_anthropometric" + visit_nutrition_program = [visit for visit in visits if visit["form_id"] == "wfp_coda_pbwg_registration"][0] if len(visit_nutrition_program) > 0: current_journey["nutrition_programme"] = visit_nutrition_program.get("physiology_status", None) diff --git a/plugins/wfp/south_sudan/management/commands/Under5.py b/plugins/wfp/south_sudan/management/commands/Under5.py index 07414bd377..d33263160c 100644 --- a/plugins/wfp/south_sudan/management/commands/Under5.py +++ b/plugins/wfp/south_sudan/management/commands/Under5.py @@ -114,7 +114,7 @@ def journeyMapper(self, visits, admission_form): "child_antropometric_followUp_tsfp", "child_antropometric_followUp_otp", ] - # admission_form = "Anthropometric visit child" + visit_nutrition_program = [visit for visit in visits if visit["form_id"] in admission_form] if len(visit_nutrition_program) > 0: current_journey["nutrition_programme"] = ETL().program_mapper(visit_nutrition_program[0]) @@ -123,17 +123,7 @@ def journeyMapper(self, visits, admission_form): def save_journey(self, beneficiary, record): journey = Journey() - journey.beneficiary = beneficiary - journey.programme_type = "U5" - journey.admission_criteria = record["admission_criteria"] - journey.admission_type = record.get("admission_type", None) - journey.nutrition_programme = record["nutrition_programme"] - journey.exit_type = record.get("exit_type", None) - journey.instance_id = record.get("instance_id", None) journey.initial_weight = record.get("initial_weight", None) - journey.start_date = record.get("start_date", None) - journey.duration = record.get("duration", None) - journey.end_date = record.get("end_date", None) # Calculate the weight gain only for cured and Transfer from OTP to TSFP cases! if ( @@ -144,8 +134,8 @@ def save_journey(self, beneficiary, record): journey.discharge_weight = record.get("discharge_weight", None) journey.weight_gain = record.get("weight_gain", 0) journey.weight_loss = record.get("weight_loss", 0) - journey.save() - return journey + + return ETL().save_entity_journey(journey, beneficiary, record, "U5") def run(self): entity_type = ETL(["child_under_5_1"]) From 23850aa95c9a7511edcdc0bc1a96ef6c925fc1d2 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Fri, 8 Nov 2024 08:39:09 +0200 Subject: [PATCH 37/46] Changing the name to reflect to the var type --- plugins/wfp/common.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/wfp/common.py b/plugins/wfp/common.py index 79a04d034c..db7ab671d4 100644 --- a/plugins/wfp/common.py +++ b/plugins/wfp/common.py @@ -5,8 +5,9 @@ class ETL: - def __init__(self, type=None): - self.type = type + + def __init__(self, types=None): + self.types = types def delete_beneficiaries(self): beneficiary = Beneficiary.objects.all().delete() @@ -17,7 +18,7 @@ def delete_beneficiaries(self): print("EXISTING JOURNEY DELETED", beneficiary[1]["wfp.Journey"]) def account_related_to_entity_type(self): - entity_type = EntityType.objects.filter(code__in=self.type) + entity_type = EntityType.objects.filter(code__in=self.types) account = Account.objects.get(id=entity_type[0].account_id) return account @@ -25,7 +26,7 @@ def retrieve_entities(self): steps_id = ETL().steps_to_exclude() updated_at = date(2023, 7, 10) beneficiaries = ( - Instance.objects.filter(entity__entity_type__code__in=self.type) + Instance.objects.filter(entity__entity_type__code__in=self.types) # .filter(entity__id__in=[1, 42, 46, 49, 58, 77, 90, 111, 322, 323, 330, 196, 226, 254,315, 424, 430, 431, 408, 19, 230, 359]) # .filter(entity__id__in=[230, 359, 254]) .filter(json__isnull=False) From 138c9711f88f8e0761d2d5464b5557e00733902f Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Fri, 8 Nov 2024 08:42:06 +0200 Subject: [PATCH 38/46] Cleaning code by removing the unnecessary comment --- plugins/wfp/common.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/wfp/common.py b/plugins/wfp/common.py index db7ab671d4..06bc2f3c9e 100644 --- a/plugins/wfp/common.py +++ b/plugins/wfp/common.py @@ -27,8 +27,6 @@ def retrieve_entities(self): updated_at = date(2023, 7, 10) beneficiaries = ( Instance.objects.filter(entity__entity_type__code__in=self.types) - # .filter(entity__id__in=[1, 42, 46, 49, 58, 77, 90, 111, 322, 323, 330, 196, 226, 254,315, 424, 430, 431, 408, 19, 230, 359]) - # .filter(entity__id__in=[230, 359, 254]) .filter(json__isnull=False) .filter(form__isnull=False) .filter(updated_at__gte=updated_at) From e818a6e742289b3d0931f80a3ce3a40ac96dba5b Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Fri, 8 Nov 2024 09:05:11 +0200 Subject: [PATCH 39/46] Improving dictionary reading by removing unnecessary conditions --- plugins/wfp/common.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/wfp/common.py b/plugins/wfp/common.py index 06bc2f3c9e..25bd72c2a9 100644 --- a/plugins/wfp/common.py +++ b/plugins/wfp/common.py @@ -397,35 +397,35 @@ def map_assistance_step(self, step, given_assistance): assistance = {"type": step.get("medicine_given"), "quantity": quantity} given_assistance.append(assistance) - if step.get("medication", None) is not None and step.get("medication", None) != "": + if step.get("medication") is not None and step.get("medication") != "": given_medication = self.split_given_medication(step.get("medication"), quantity) given_assistance = given_assistance + given_medication - if step.get("medicine_given_2") is not None: + if step.get("medicine_given_2") is not None and step.get("medicine_given_2") != "": assistance = {"type": step.get("medicine_given_2"), "quantity": quantity} given_assistance.append(assistance) - if step.get("medication_2", None) is not None and step.get("medication_2", None) != "": + if step.get("medication_2") is not None and step.get("medication_2") != "": given_medication = self.split_given_medication(step.get("medication_2"), quantity) given_assistance = given_assistance + given_medication - if step.get("vitamins_given", None) is not None and step.get("vitamins_given", None) == "1": + if step.get("vitamins_given") == "1": assistance = {"type": "Vitamin", "quantity": quantity} given_assistance.append(assistance) - if step.get("ab_given", None) is not None and step.get("ab_given", None) == "1": + if step.get("ab_given") == "1": assistance = {"type": "albendazole", "quantity": quantity} given_assistance.append(assistance) - if step.get("measles_vacc", None) is not None and step.get("measles_vacc", None) == "1": + if step.get("measles_vacc") == "1": assistance = {"type": "Measles vaccination", "quantity": quantity} given_assistance.append(assistance) - if step.get("art_given", None) is not None and step.get("art_given", None) == "1": + if step.get("art_given") == "1": assistance = {"type": "ART", "quantity": quantity} given_assistance.append(assistance) - if step.get("anti_helminth_given", None) is not None and step.get("anti_helminth_given", None) != "": + if step.get("anti_helminth_given") is not None and step.get("anti_helminth_given") != "": assistance = {"type": step.get("anti_helminth_given"), "quantity": quantity} given_assistance.append(assistance) From 5b91a4e217b5f6bef3ab29450e40ced6a26deb8c Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Fri, 8 Nov 2024 09:21:58 +0200 Subject: [PATCH 40/46] Check the ration type --- plugins/wfp/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wfp/common.py b/plugins/wfp/common.py index 25bd72c2a9..9de055e99e 100644 --- a/plugins/wfp/common.py +++ b/plugins/wfp/common.py @@ -462,7 +462,7 @@ def map_assistance_step(self, step, given_assistance): "quantity": quantity, } given_assistance.append(assistance) - elif step.get("ration_type") is not None: + elif step.get("ration_type"): if step.get("ration_type") in ["csb", "csb1", "csb2"]: quantity = step.get("_csb_packets") elif step.get("ration_type") == "lndf": From 3abab394017e69a3b72542198a6877a9311f0252 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Fri, 8 Nov 2024 09:23:57 +0200 Subject: [PATCH 41/46] Formatting code with black --- plugins/wfp/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/wfp/common.py b/plugins/wfp/common.py index 9de055e99e..1b16b59818 100644 --- a/plugins/wfp/common.py +++ b/plugins/wfp/common.py @@ -5,7 +5,6 @@ class ETL: - def __init__(self, types=None): self.types = types From 994493098b82b6eacb587fa5232cf363709c5548 Mon Sep 17 00:00:00 2001 From: Bram Jans Date: Fri, 8 Nov 2024 14:04:24 +0100 Subject: [PATCH 42/46] WC2-592 Refactor ETL directory structure --- hat/settings.py | 2 -- plugins/wfp/{nigeria => }/management/commands/etl_ng.py | 2 +- .../wfp/{south_sudan => }/management/commands/etl_ssd.py | 2 +- .../commands => management/commands/nigeria}/Under5.py | 4 ++-- .../commands => management/commands/south_sudan}/Pbwg.py | 8 ++++---- .../commands/south_sudan}/Under5.py | 8 ++++---- .../commands/south_sudan}/wfp_random_data.py | 4 ++-- plugins/wfp/tasks.py | 6 +++--- 8 files changed, 17 insertions(+), 19 deletions(-) rename plugins/wfp/{nigeria => }/management/commands/etl_ng.py (85%) rename plugins/wfp/{south_sudan => }/management/commands/etl_ssd.py (85%) rename plugins/wfp/{nigeria/management/commands => management/commands/nigeria}/Under5.py (99%) rename plugins/wfp/{south_sudan/management/commands => management/commands/south_sudan}/Pbwg.py (98%) rename plugins/wfp/{south_sudan/management/commands => management/commands/south_sudan}/Under5.py (98%) rename plugins/wfp/{south_sudan/management/commands => management/commands/south_sudan}/wfp_random_data.py (69%) diff --git a/hat/settings.py b/hat/settings.py index 4d3c846f4c..97e051be6d 100644 --- a/hat/settings.py +++ b/hat/settings.py @@ -177,8 +177,6 @@ "allauth.account", "allauth.socialaccount", "storages", - "plugins.wfp.south_sudan", - "plugins.wfp.nigeria", ] if ENABLE_CORS: INSTALLED_APPS += [ diff --git a/plugins/wfp/nigeria/management/commands/etl_ng.py b/plugins/wfp/management/commands/etl_ng.py similarity index 85% rename from plugins/wfp/nigeria/management/commands/etl_ng.py rename to plugins/wfp/management/commands/etl_ng.py index 1d7d4b5761..1260215943 100644 --- a/plugins/wfp/nigeria/management/commands/etl_ng.py +++ b/plugins/wfp/management/commands/etl_ng.py @@ -1,5 +1,5 @@ from django.core.management.base import BaseCommand -from ....tasks import etl_ng +from plugins.wfp.tasks import etl_ng class Command(BaseCommand): diff --git a/plugins/wfp/south_sudan/management/commands/etl_ssd.py b/plugins/wfp/management/commands/etl_ssd.py similarity index 85% rename from plugins/wfp/south_sudan/management/commands/etl_ssd.py rename to plugins/wfp/management/commands/etl_ssd.py index 3facfdeb7b..1315d79118 100644 --- a/plugins/wfp/south_sudan/management/commands/etl_ssd.py +++ b/plugins/wfp/management/commands/etl_ssd.py @@ -1,5 +1,5 @@ from django.core.management.base import BaseCommand -from ....tasks import etl_ssd +from plugins.wfp.tasks import etl_ssd class Command(BaseCommand): diff --git a/plugins/wfp/nigeria/management/commands/Under5.py b/plugins/wfp/management/commands/nigeria/Under5.py similarity index 99% rename from plugins/wfp/nigeria/management/commands/Under5.py rename to plugins/wfp/management/commands/nigeria/Under5.py index 3833f8efa7..6651878fe1 100644 --- a/plugins/wfp/nigeria/management/commands/Under5.py +++ b/plugins/wfp/management/commands/nigeria/Under5.py @@ -1,9 +1,9 @@ -from ....models import * +from plugins.wfp.models import * from iaso.models import * from django.core.management.base import BaseCommand from itertools import groupby from operator import itemgetter -from ....common import ETL +from plugins.wfp.common import ETL import logging logger = logging.getLogger(__name__) diff --git a/plugins/wfp/south_sudan/management/commands/Pbwg.py b/plugins/wfp/management/commands/south_sudan/Pbwg.py similarity index 98% rename from plugins/wfp/south_sudan/management/commands/Pbwg.py rename to plugins/wfp/management/commands/south_sudan/Pbwg.py index 2536dbdc59..a1e1c39179 100644 --- a/plugins/wfp/south_sudan/management/commands/Pbwg.py +++ b/plugins/wfp/management/commands/south_sudan/Pbwg.py @@ -1,9 +1,9 @@ -from ....models import * -from django.core.management.base import BaseCommand +import logging from itertools import groupby from operator import itemgetter -from ....common import ETL -import logging + +from plugins.wfp.common import ETL +from plugins.wfp.models import * logger = logging.getLogger(__name__) diff --git a/plugins/wfp/south_sudan/management/commands/Under5.py b/plugins/wfp/management/commands/south_sudan/Under5.py similarity index 98% rename from plugins/wfp/south_sudan/management/commands/Under5.py rename to plugins/wfp/management/commands/south_sudan/Under5.py index d33263160c..92b60efc62 100644 --- a/plugins/wfp/south_sudan/management/commands/Under5.py +++ b/plugins/wfp/management/commands/south_sudan/Under5.py @@ -1,9 +1,9 @@ -from ....models import * -from django.core.management.base import BaseCommand +import logging from itertools import groupby from operator import itemgetter -from ....common import ETL -import logging + +from plugins.wfp.common import ETL +from plugins.wfp.models import * logger = logging.getLogger(__name__) diff --git a/plugins/wfp/south_sudan/management/commands/wfp_random_data.py b/plugins/wfp/management/commands/south_sudan/wfp_random_data.py similarity index 69% rename from plugins/wfp/south_sudan/management/commands/wfp_random_data.py rename to plugins/wfp/management/commands/south_sudan/wfp_random_data.py index 6def46b706..4c00f23d61 100644 --- a/plugins/wfp/south_sudan/management/commands/wfp_random_data.py +++ b/plugins/wfp/management/commands/south_sudan/wfp_random_data.py @@ -1,7 +1,7 @@ -from ....models import * # type: ignore from django.core.management.base import BaseCommand -from ....tasks import generate_random_data +from plugins.wfp.models import * # type: ignore +from plugins.wfp.tasks import generate_random_data class Command(BaseCommand): diff --git a/plugins/wfp/tasks.py b/plugins/wfp/tasks.py index 0e6aac8275..1b5b920c7b 100644 --- a/plugins/wfp/tasks.py +++ b/plugins/wfp/tasks.py @@ -4,9 +4,9 @@ import random from celery import shared_task import datetime -from .south_sudan.management.commands.Under5 import Under5 -from .south_sudan.management.commands.Pbwg import PBWG -from .nigeria.management.commands.Under5 import NG_Under5 +from .management.commands.south_sudan.Under5 import Under5 +from .management.commands.south_sudan.Pbwg import PBWG +from .management.commands.nigeria.Under5 import NG_Under5 import logging logger = logging.getLogger(__name__) From 3c3e6e24be3b27c1f4a3e695d48dd036793e08a5 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Fri, 8 Nov 2024 15:45:12 +0200 Subject: [PATCH 43/46] Update the readme with avalaible celery tasks --- plugins/wfp/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wfp/README.md b/plugins/wfp/README.md index 5a39b6dedc..eab4059d97 100644 --- a/plugins/wfp/README.md +++ b/plugins/wfp/README.md @@ -22,8 +22,8 @@ Both have default value of `redis://localhost:6379` 6. You now have two celery task that can be triggered: ``` - plugins.wfp.tasks.etl - plugins.wfp.tasks.generate_random_data + plugins.wfp.tasks.etl_ssd + plugins.wfp.tasks.etl_ng ``` 7. Run python test `docker compose run iaso manage test -k ETL` From 56fd3757d2f699187ceea9e76887902bf7b25d01 Mon Sep 17 00:00:00 2001 From: Fleury Butoyi Date: Fri, 8 Nov 2024 15:49:53 +0200 Subject: [PATCH 44/46] Removing random generate data task --- plugins/wfp/tasks.py | 69 -------------------------------------------- 1 file changed, 69 deletions(-) diff --git a/plugins/wfp/tasks.py b/plugins/wfp/tasks.py index 1b5b920c7b..ee97b53faa 100644 --- a/plugins/wfp/tasks.py +++ b/plugins/wfp/tasks.py @@ -1,9 +1,6 @@ from .models import * from iaso.models import * -from datetime import timedelta -import random from celery import shared_task -import datetime from .management.commands.south_sudan.Under5 import Under5 from .management.commands.south_sudan.Pbwg import PBWG from .management.commands.nigeria.Under5 import NG_Under5 @@ -12,72 +9,6 @@ logger = logging.getLogger(__name__) -@shared_task() -def generate_random_data(): - """Insert random data in the database for 2000 beneficiaries""" - admission_types = [t[0] for t in ADMISSION_TYPES] - - facilities = OrgUnit.objects.filter(org_unit_type=1) - for i in range(2000): - if i % 10 == 0: - print("Inserted %d beneficiaries" % i) - b = Beneficiary() - b.gender = random.choice(["male", "female"]) - random_birth = random.randint(1, 1825) - b.birth_date = datetime.datetime.utcnow() - timedelta(days=random_birth) - b.save() - - journey_count = random.randint(1, 3) - - for j in range(journey_count): - journey = Journey() - journey.beneficiary = b - journey.nutrition_programme = random.choice(["TSFP", "OTP"]) - journey.admission_criteria = random.choice(["WHZ", "MUAC"]) - journey.admission_type = random.choice(admission_types) - journey.weight_gain = random.randint(-1000, 5000) / 1000.0 - journey.exit_type = random.choice( - [ - "cured", - "default", - "non-respondent", - "death", - "refered-for-medical-investigation", - "referred-to-sc-itp", - "volontary-withdrawal", - "dismissal-cheating", - ] - ) - r = random.randint(1, 5) - journey.programme_type = "PLW" if r < 2 else "U5" - - journey.save() - - visit_count = random.randint(1, 6) - - number = 1 - - visit_offsets = [random.randint(1, random_birth) for l in range(visit_count)] - visit_offsets.sort() - - facility = random.choice(facilities) - for k in range(visit_count): - visit = Visit() - visit.number = number - visit.org_unit = facility - number += 1 - visit.date = b.birth_date + timedelta(days=visit_offsets[k]) - visit.journey = journey - - visit.save() - - step = Step() - step.assistance_type = random.choice(["RUSF", "RUTF", "CSB++", ""]) - step.quantity_given = random.randint(1, 20) - step.visit = visit - step.save() - - @shared_task() def etl_ng(): """Extract beneficiary data from Iaso tables and store them in the format expected by existing tableau dashboards""" From 9d152da974fac93115fb110eea45ee9dc0c65e72 Mon Sep 17 00:00:00 2001 From: Christophe Gerard Date: Fri, 8 Nov 2024 16:59:58 +0100 Subject: [PATCH 45/46] fix migrations issues --- .../Iaso/components/files/pdf/DocumentUploadWithPreview.tsx | 2 +- .../StockManagement/StockVariation/Modals/CreateEditFormA.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx b/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx index 052c805334..52476d4acd 100644 --- a/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx +++ b/hat/assets/js/apps/Iaso/components/files/pdf/DocumentUploadWithPreview.tsx @@ -36,7 +36,7 @@ const DocumentUploadWithPreview: React.FC = ({ Date: Tue, 12 Nov 2024 12:30:36 +0100 Subject: [PATCH 46/46] Change case of "o" to lowercase --- iaso/api/fixtures/sample_bulk_user_creation.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iaso/api/fixtures/sample_bulk_user_creation.csv b/iaso/api/fixtures/sample_bulk_user_creation.csv index 1ef4e19264..5374075f00 100644 --- a/iaso/api/fixtures/sample_bulk_user_creation.csv +++ b/iaso/api/fixtures/sample_bulk_user_creation.csv @@ -1,2 +1,2 @@ -username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,Organization,projects,user_roles,phone_number,editable_org_unit_types +username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,organization,projects,user_roles,phone_number,editable_org_unit_types user name should not contain whitespaces,"Min. 8 characters, should include 1 letter and 1 number",,,,Use Org Unit ID to avoid errors,Org Unit external ID,"Possible values: iaso_forms,iaso_mappings,iaso_completeness,iaso_org_units,iaso_links,iaso_users,iaso_pages,iaso_projects,iaso_sources,iaso_data_tasks,iaso_submissions,iaso_update_submission,iaso_planning,iaso_reports,iaso_teams,iaso_assignments,iaso_entities,iaso_storages,iaso_completeness_stats,iaso_workflows,iaso_registry","Possible values: EN, FR",Optional,Optional,projects,user roles,The phone number as a string (in single or double quote). It has to start with the country code like +1 for US,"Use comma separated Org Unit Type IDs to avoid errors: 1, 2"