diff --git a/aws/backend.json b/aws/backend.json index 145b36061b..8b4d9bf195 100644 --- a/aws/backend.json +++ b/aws/backend.json @@ -275,18 +275,6 @@ "valueFrom": "/care/backend/ABDM_CLIENT_SECRET", "name": "ABDM_CLIENT_SECRET" }, - { - "valueFrom": "/care/backend/PLAUSIBLE_HOST", - "name": "PLAUSIBLE_HOST" - }, - { - "valueFrom": "/care/backend/PLAUSIBLE_SITE_ID", - "name": "PLAUSIBLE_SITE_ID" - }, - { - "valueFrom": "/care/backend/PLAUSIBLE_AUTH_TOKEN", - "name": "PLAUSIBLE_AUTH_TOKEN" - }, { "valueFrom": "/care/backend/JWKS_BASE64", "name": "JWKS_BASE64" diff --git a/aws/celery.json b/aws/celery.json index 197c6d7346..fdaa13d908 100644 --- a/aws/celery.json +++ b/aws/celery.json @@ -250,18 +250,6 @@ "valueFrom": "/care/backend/HCX_CERT_URL", "name": "HCX_CERT_URL" }, - { - "valueFrom": "/care/backend/PLAUSIBLE_HOST", - "name": "PLAUSIBLE_HOST" - }, - { - "valueFrom": "/care/backend/PLAUSIBLE_SITE_ID", - "name": "PLAUSIBLE_SITE_ID" - }, - { - "valueFrom": "/care/backend/PLAUSIBLE_AUTH_TOKEN", - "name": "PLAUSIBLE_AUTH_TOKEN" - }, { "valueFrom": "/care/backend/ABDM_CLIENT_ID", "name": "ABDM_CLIENT_ID" @@ -525,18 +513,6 @@ "valueFrom": "/care/backend/HCX_CERT_URL", "name": "HCX_CERT_URL" }, - { - "valueFrom": "/care/backend/PLAUSIBLE_HOST", - "name": "PLAUSIBLE_HOST" - }, - { - "valueFrom": "/care/backend/PLAUSIBLE_SITE_ID", - "name": "PLAUSIBLE_SITE_ID" - }, - { - "valueFrom": "/care/backend/PLAUSIBLE_AUTH_TOKEN", - "name": "PLAUSIBLE_AUTH_TOKEN" - }, { "valueFrom": "/care/backend/ABDM_CLIENT_ID", "name": "ABDM_CLIENT_ID" diff --git a/care/emr/resources/patient/spec.py b/care/emr/resources/patient/spec.py index 6fc2a7aa2b..cdbef580e0 100644 --- a/care/emr/resources/patient/spec.py +++ b/care/emr/resources/patient/spec.py @@ -3,7 +3,7 @@ from enum import Enum from django.utils import timezone -from pydantic import UUID4, field_validator, model_validator +from pydantic import UUID4, Field, field_validator, model_validator from care.emr.models import Organization from care.emr.models.patient import Patient @@ -36,8 +36,8 @@ class PatientBaseSpec(EMRResource): id: UUID4 | None = None name: str gender: GenderChoices - phone_number: str - emergency_phone_number: str | None = None + phone_number: str = Field(max_length=14) + emergency_phone_number: str | None = Field(None, max_length=14) address: str permanent_address: str pincode: int diff --git a/care/emr/tests/__init__.py b/care/emr/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/emr/tests/test_patient_api.py b/care/emr/tests/test_patient_api.py new file mode 100644 index 0000000000..9e354e7764 --- /dev/null +++ b/care/emr/tests/test_patient_api.py @@ -0,0 +1,81 @@ +from django.urls import reverse +from polyfactory.factories.pydantic_factory import ModelFactory +from rest_framework import status + +from care.emr.resources.patient.spec import PatientCreateSpec +from care.security.permissions.patient import PatientPermissions +from care.utils.tests.base import CareAPITestBase + + +class PatientFactory(ModelFactory[PatientCreateSpec]): + __model__ = PatientCreateSpec + + +class TestPatientViewSet(CareAPITestBase): + """ + Test cases for checking Patient CRUD operations + + Tests check if: + 1. Permissions are enforced for all operations + 2. Data validations work + 3. Proper responses are returned + 4. Filters work as expected + """ + + def setUp(self): + """Set up test data that's needed for all tests""" + super().setUp() # Call parent's setUp to ensure proper initialization + self.base_url = reverse("patient-list") + + def generate_patient_data(self, **kwargs): + if "age" not in kwargs and "date_of_birth" not in kwargs: + kwargs["age"] = self.fake.random_int(min=1, max=100) + return PatientFactory.build(meta={}, **kwargs) + + def test_create_patient_unauthenticated(self): + """Test that unauthenticated users cannot create patients""" + response = self.client.post(self.base_url, {}, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_create_empty_patient_validation(self): + """Test validation when creating patient with empty data""" + user = self.create_user() + self.client.force_authenticate(user=user) + response = self.client.post(self.base_url, {}, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_patient_authorization(self): + """Test patient creation with proper authorization""" + user = self.create_user() + geo_organization = self.create_organization(org_type="govt") + patient_data = self.generate_patient_data( + geo_organization=geo_organization.external_id + ) + organization = self.create_organization(org_type="govt") + role = self.create_role_with_permissions( + permissions=[PatientPermissions.can_create_patient.name] + ) + self.attach_role_organization_user(organization, user, role) + self.client.force_authenticate(user=user) + response = self.client.post( + self.base_url, patient_data.model_dump(mode="json"), format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_create_patient_unauthorization(self): + """Test patient creation with proper authorization""" + user = self.create_user() + geo_organization = self.create_organization(org_type="govt") + patient_data = self.generate_patient_data( + geo_organization=geo_organization.external_id + ) + organization = self.create_organization(org_type="govt") + role = self.create_role_with_permissions( + permissions=[PatientPermissions.can_list_patients.name] + ) + self.attach_role_organization_user(organization, user, role) + self.client.force_authenticate(user=user) + response = self.client.post( + self.base_url, patient_data.model_dump(mode="json"), format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/care/facility/tasks/__init__.py b/care/facility/tasks/__init__.py index e989463c46..2f7213166f 100644 --- a/care/facility/tasks/__init__.py +++ b/care/facility/tasks/__init__.py @@ -1,32 +1,25 @@ -from celery import current_app -from celery.schedules import crontab +# from celery import current_app +# from celery.schedules import crontab -from care.facility.tasks.asset_monitor import check_asset_status -from care.facility.tasks.cleanup import delete_old_notifications -from care.facility.tasks.location_monitor import check_location_status -from care.facility.tasks.plausible_stats import capture_goals +# from care.facility.tasks.asset_monitor import check_asset_status +# from care.facility.tasks.cleanup import delete_old_notifications +# from care.facility.tasks.location_monitor import check_location_status +# @current_app.on_after_finalize.connect +# def setup_periodic_tasks(sender, **kwargs): +# sender.add_periodic_task( +# crontab(hour="0", minute="0"), +# delete_old_notifications.s(), +# name="delete_old_notifications", +# ) -@current_app.on_after_finalize.connect -def setup_periodic_tasks(sender, **kwargs): - sender.add_periodic_task( - crontab(hour="0", minute="0"), - delete_old_notifications.s(), - name="delete_old_notifications", - ) - - sender.add_periodic_task( - crontab(minute="*/30"), - check_asset_status.s(), - name="check_asset_status", - ) - sender.add_periodic_task( - crontab(hour="0", minute="0"), - capture_goals.s(), - name="capture_goals", - ) - sender.add_periodic_task( - crontab(minute="*/30"), - check_location_status.s(), - name="check_location_status", - ) +# sender.add_periodic_task( +# crontab(minute="*/30"), +# check_asset_status.s(), +# name="check_asset_status", +# ) +# sender.add_periodic_task( +# crontab(minute="*/30"), +# check_location_status.s(), +# name="check_location_status", +# ) diff --git a/care/facility/tasks/plausible_stats.py b/care/facility/tasks/plausible_stats.py deleted file mode 100644 index c8f4660743..0000000000 --- a/care/facility/tasks/plausible_stats.py +++ /dev/null @@ -1,155 +0,0 @@ -import logging -from datetime import timedelta -from enum import Enum - -import requests -from celery import shared_task -from django.conf import settings -from django.utils.timezone import now - -from care.facility.models.stats import Goal, GoalEntry, GoalProperty, GoalPropertyEntry - -logger = logging.getLogger(__name__) - - -class Goals(Enum): - PATIENT_CONSULTATION_VIEWED = ("facilityId", "consultationId", "userId") - DOCTOR_CONNECT_CLICKED = ("consultationId", "facilityId", "userId", "page") - CAMERA_PRESET_CLICKED = ("presetName", "consultationId", "userId", "result") - CAMERA_FEED_MOVED = ("direction", "consultationId", "userId") - PATIENT_PROFILE_VIEWED = ("facilityId", "userId") - DEVICE_VIEWED = ("bedId", "assetId", "userId") - PAGEVIEW = ("page",) - - @property - def formatted_name(self): - if self == Goals.PAGEVIEW: - return "pageview" # pageview is a reserved goal in plausible - return self.name.replace("_", " ").title() - - -def get_goal_stats(plausible_host, site_id, date, goal_name): - goal_filter = f"event:name=={goal_name}" - url = f"https://{plausible_host}/api/v1/stats/aggregate" - - params = { - "site_id": site_id, - "filters": goal_filter, - "period": "day", - "date": date, - "metrics": "visitors,events", - } - - response = requests.get( - url, - params=params, - headers={ - "Authorization": "Bearer " + settings.PLAUSIBLE_AUTH_TOKEN, - }, - timeout=60, - ) - - response.raise_for_status() - - return response.json() - - -def get_goal_event_stats(plausible_host, site_id, date, goal_name, event_name): - goal_filter = f"event:name=={goal_name}" - - # pageview is a reserved goal in plausible which uses event:page - if goal_name == "pageview" and event_name == "page": - goal_event = "event:page" - else: - goal_event = f"event:props:{event_name}" - - url = f"https://{plausible_host}/api/v1/stats/breakdown" - - params = { - "site_id": site_id, - "property": goal_event, - "filters": goal_filter, - "period": "day", - "date": date, - "metrics": "visitors,events", - } - - response = requests.get( - url, - params=params, - headers={ - "Authorization": "Bearer " + settings.PLAUSIBLE_AUTH_TOKEN, - }, - timeout=60, - ) - - response.raise_for_status() - - return response.json() - - -@shared_task -def capture_goals(): - if ( - not settings.PLAUSIBLE_HOST - or not settings.PLAUSIBLE_SITE_ID - or not settings.PLAUSIBLE_AUTH_TOKEN - ): - logger.info("Plausible is not configured, skipping") - return - today = now().date() - yesterday = today - timedelta(days=1) - logger.info("Capturing Goals for %s", yesterday) - - for goal in Goals: - try: - goal_name = goal.formatted_name - goal_data = get_goal_stats( - settings.PLAUSIBLE_HOST, - settings.PLAUSIBLE_SITE_ID, - yesterday, - goal_name, - ) - goal_object, _ = Goal.objects.get_or_create( - name=goal_name, - ) - goal_entry_object, _ = GoalEntry.objects.get_or_create( - goal=goal_object, - date=yesterday, - ) - goal_entry_object.visitors = goal_data["results"]["visitors"]["value"] - goal_entry_object.events = goal_data["results"]["events"]["value"] - goal_entry_object.save() - - logger.info("Saved goal entry for %s on %s", goal_name, yesterday) - - for property_name in goal.value: - goal_property_stats = get_goal_event_stats( - settings.PLAUSIBLE_HOST, - settings.PLAUSIBLE_SITE_ID, - yesterday, - goal_name, - property_name, - ) - for property_statistic in goal_property_stats["results"]: - property_object, _ = GoalProperty.objects.get_or_create( - goal=goal_object, - name=property_name, - ) - property_entry_object, _ = GoalPropertyEntry.objects.get_or_create( - goal_property=property_object, - goal_entry=goal_entry_object, - value=property_statistic[property_name], - ) - property_entry_object.visitors = property_statistic["visitors"] - property_entry_object.events = property_statistic["events"] - property_entry_object.save() - logger.info( - "Saved goal property entry for %s and property %s on %s", - goal_name, - property_name, - yesterday, - ) - - except Exception as e: - logger.error("Failed to process goal %s due to error: %s", goal_name, e) diff --git a/care/security/authorization/patient.py b/care/security/authorization/patient.py index 39ffb3bef4..707f6d18ca 100644 --- a/care/security/authorization/patient.py +++ b/care/security/authorization/patient.py @@ -70,6 +70,8 @@ def can_submit_questionnaire_patient_obj(self, user, patient): def can_create_patient(self, user): return self.check_permission_in_facility_organization( [PatientPermissions.can_create_patient.name], user + ) or self.check_permission_in_organization( + [PatientPermissions.can_create_patient.name], user ) def can_view_clinical_data(self, user, patient): diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index 7b4ec3632f..f0c503f7b2 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -6,7 +6,6 @@ from care.emr.models import Organization from care.emr.models.organization import FacilityOrganizationUser, OrganizationUser from care.emr.resources.organization.spec import OrganizationReadSpec -from care.emr.resources.role.spec import PermissionSpec from care.facility.api.serializers.facility import FacilityBareMinimumSerializer from care.facility.models import Facility, FacilityUser from care.security.models import RolePermission @@ -306,9 +305,7 @@ def get_permissions(self, user): "role_id", flat=True ) ).select_related("permission") - return [ - PermissionSpec.serialize(obj.permission).to_json() for obj in permissions - ] + return [obj.permission.slug for obj in permissions] def get_facilities(self, user): unique_ids = [] diff --git a/care/users/tests/test_user_create.py b/care/users/tests/test_user_create.py index 033d8e9b71..9cd3b81dbe 100644 --- a/care/users/tests/test_user_create.py +++ b/care/users/tests/test_user_create.py @@ -1,5 +1,3 @@ -import logging - from django.urls import reverse from polyfactory.factories.pydantic_factory import ModelFactory from rest_framework import status @@ -50,7 +48,8 @@ def test_create_user_authorization(self): name=UserTypeRoleMapping[new_user.user_type.value].value.name, is_system=True, ) - logging.info(UserTypeRoleMapping[new_user.user_type.value].value.name) self.client.force_authenticate(user=user) - response = self.client.post(self.base_url, new_user.dict(), format="json") + response = self.client.post( + self.base_url, new_user.model_dump(mode="json"), format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/care/utils/tests/base.py b/care/utils/tests/base.py index 4ea8667a8b..f765decfe3 100644 --- a/care/utils/tests/base.py +++ b/care/utils/tests/base.py @@ -39,6 +39,11 @@ def create_role_with_permissions(self, permissions): ) return role + def create_patient(self, **kwargs): + from care.emr.models import Patient + + return baker.make(Patient, **kwargs) + def attach_role_organization_user(self, organization, user, role): OrganizationUser.objects.create(organization=organization, user=user, role=role) diff --git a/config/settings/base.py b/config/settings/base.py index 7da541c30f..becf4c8561 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -640,11 +640,6 @@ APP_VERSION = env("APP_VERSION", default="unknown") IS_PRODUCTION = False - -PLAUSIBLE_HOST = env("PLAUSIBLE_HOST", default="") -PLAUSIBLE_SITE_ID = env("PLAUSIBLE_SITE_ID", default="") -PLAUSIBLE_AUTH_TOKEN = env("PLAUSIBLE_AUTH_TOKEN", default="") - # Timeout for middleware request (in seconds) MIDDLEWARE_REQUEST_TIMEOUT = env.int("MIDDLEWARE_REQUEST_TIMEOUT", 20)