Skip to content

added option to unmark patient deceased#3542

Open
praffq wants to merge 3 commits intodevelopfrom
prafful/adding-option-to-unmark-deceased-patient
Open

added option to unmark patient deceased#3542
praffq wants to merge 3 commits intodevelopfrom
prafful/adding-option-to-unmark-deceased-patient

Conversation

@praffq
Copy link
Contributor

@praffq praffq commented Feb 23, 2026

Proposed Changes

Merge Checklist

  • Tests added/fixed
  • Update docs in /docs
  • Linting Complete
  • Any other necessary step

Only PR's with test cases included and passing lint and test pipelines will be reviewed

@ohcnetwork/care-backend-maintainers @ohcnetwork/care-backend-admins

Summary by CodeRabbit

  • New Features

    • Only authorized users (superusers) can reverse a patient's deceased status.
    • Patient updates now clear the deceased timestamp when the field is omitted, ensuring form-driven updates behave predictably.
  • Tests

    • Added tests covering allowed and forbidden attempts to unmark deceased patients and related update flows.

@praffq praffq requested a review from a team as a code owner February 23, 2026 07:58
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Adds a permission and authorization method to prevent clearing a patient's deceased_datetime by unauthorized users, ensures deserialization can reset deceased_datetime to None, and adds tests for allowed and forbidden unmarking flows.

Changes

Cohort / File(s) Summary
Authorization & Permissions
care/security/authorization/patient.py
Adds can_unmark_deceased_patient(self, user) to PatientAccess, permitting only superusers to reverse deceased status.
API Update
care/emr/api/viewsets/patient.py
Adds a check in PatientViewSet.perform_update to raise PermissionDenied if an update unsets deceased_datetime while the user lacks can_unmark_deceased_patient.
Deserialization / Spec
care/emr/resources/patient/spec.py
In PatientUpdateSpec.perform_extra_deserialization, sets obj.deceased_datetime = None when self.deceased_datetime is not provided during updates.
Tests
care/emr/tests/test_patient_api.py
Adds tests: superuser can unmark deceased patient; non-superuser attempting to unmark receives 403; adjusts non-superuser permission setup around patient deletion/update tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description references the issue but lacks details about the proposed changes, omits architecture changes context, and the merge checklist items remain unchecked despite the PR including tests and presumably passing linting. Add a brief explanation of what changes were made (permission checks, API endpoints modified) and check off the completed checklist items like 'Tests added/fixed' and 'Linting Complete' to clarify PR readiness.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding functionality to unmark a deceased patient, which aligns with the code changes across viewsets, permissions, and tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch prafful/adding-option-to-unmark-deceased-patient

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
care/emr/api/viewsets/patient.py (1)

158-166: Consider caching the result of self.get_object() to avoid duplicate database queries.

The call to self.get_object() on line 160 fetches the patient from the database. If this method is called elsewhere in the update flow (or if super().perform_update() also fetches it), you're making redundant queries. You might want to store the result and reuse it.

That said, the permission logic itself looks correct—nicely preventing unauthorized reversal of deceased status.

♻️ Proposed optimization
 def perform_update(self, instance):
     identifiers = instance._identifiers  # noqa: SLF001
+    existing_obj = self.get_object()
     with transaction.atomic():
         if (
             instance.deceased_datetime is None
-            and self.get_object().deceased_datetime
+            and existing_obj.deceased_datetime
             and not AuthorizationController.call(
                 "can_unmark_deceased_patient", self.request.user
             )
         ):
             raise PermissionDenied(detail="Cannot mark deceased patient alive")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/api/viewsets/patient.py` around lines 158 - 166, Store the result of
self.get_object() in a local variable and reuse it instead of calling it twice;
e.g., in the viewset update/perform_update flow, call existing =
self.get_object() once, then compare instance.deceased_datetime to
existing.deceased_datetime and call
AuthorizationController.call("can_unmark_deceased_patient", self.request.user)
as before, raising PermissionDenied(...) if unauthorized—this eliminates the
duplicate database fetch and keeps the same logic in
perform_update/get_object/AuthorizationController.call.
care/emr/tests/test_patient_api.py (1)

209-270: Good test coverage for the primary use cases.

These tests correctly validate the permission boundary for unmarking deceased patients. The superuser test confirms authorized access, and the non-superuser test confirms the 403 response. Nicely done.

One thing you might want to add: a test verifying that a partial update (one that doesn't include deceased_datetime at all) doesn't accidentally clear the deceased status. Given the implementation in spec.py, this edge case could expose unexpected behavior.

💡 Suggested additional test case
def test_update_patient_without_deceased_datetime_preserves_status(self):
    """Test that updating a patient without providing deceased_datetime doesn't clear it"""
    geo_organization = self.create_organization(org_type="govt")
    superuser = self.create_super_user()
    role = self.create_role_with_permissions(
        permissions=[
            PatientPermissions.can_create_patient.name,
            PatientPermissions.can_write_patient.name,
            PatientPermissions.can_list_patients.name,
        ]
    )
    self.attach_role_organization_user(geo_organization, superuser, role)
    self.client.force_authenticate(user=superuser)

    # Create patient with deceased_datetime
    deceased_time = care_now() - datetime.timedelta(days=2)
    patient_data = self.generate_patient_data(
        geo_organization=geo_organization.external_id,
        deceased_datetime=deceased_time,
    )
    response = self.client.post(self.base_url, patient_data, format="json")
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    patient_id = response.data["id"]

    # Update patient WITHOUT including deceased_datetime
    update_url = reverse("patient-detail", kwargs={"external_id": patient_id})
    del patient_data["deceased_datetime"]
    response = self.client.put(update_url, patient_data, format="json")
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    # This SHOULD still have the original deceased_datetime
    self.assertIsNotNone(response.data["deceased_datetime"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/tests/test_patient_api.py` around lines 209 - 270, Add a test named
test_update_patient_without_deceased_datetime_preserves_status that follows the
existing pattern: use
create_organization/create_super_user/create_role_with_permissions/attach_role_organization_user
and client.force_authenticate, create a patient via POST using
generate_patient_data with deceased_datetime set, then perform a PUT to
reverse("patient-detail", kwargs={"external_id": patient_id}) after removing the
"deceased_datetime" key from the payload (del patient_data["deceased_datetime"])
and assert status 200 and that response.data["deceased_datetime"] is still not
None to ensure partial updates don't clear the field.
ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c30dc8 and f57cbc3.

📒 Files selected for processing (4)
  • care/emr/api/viewsets/patient.py
  • care/emr/resources/patient/spec.py
  • care/emr/tests/test_patient_api.py
  • care/security/authorization/patient.py
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@care/emr/resources/patient/spec.py`:
- Around line 190-191: The current code unconditionally clears
obj.deceased_datetime when self.deceased_datetime is None; change the logic to
only modify obj.deceased_datetime if the field was explicitly provided in the
input. For example, detect explicit presence using the Pydantic model's
__fields_set__ (e.g., if "deceased_datetime" in self.__fields_set__: set
obj.deceased_datetime = self.deceased_datetime), or implement a sentinel/default
marker for omitted fields and only assign when the field is present; update the
block that references self.deceased_datetime and obj.deceased_datetime
accordingly.

---

Nitpick comments:
In `@care/emr/api/viewsets/patient.py`:
- Around line 158-166: Store the result of self.get_object() in a local variable
and reuse it instead of calling it twice; e.g., in the viewset
update/perform_update flow, call existing = self.get_object() once, then compare
instance.deceased_datetime to existing.deceased_datetime and call
AuthorizationController.call("can_unmark_deceased_patient", self.request.user)
as before, raising PermissionDenied(...) if unauthorized—this eliminates the
duplicate database fetch and keeps the same logic in
perform_update/get_object/AuthorizationController.call.

In `@care/emr/tests/test_patient_api.py`:
- Around line 209-270: Add a test named
test_update_patient_without_deceased_datetime_preserves_status that follows the
existing pattern: use
create_organization/create_super_user/create_role_with_permissions/attach_role_organization_user
and client.force_authenticate, create a patient via POST using
generate_patient_data with deceased_datetime set, then perform a PUT to
reverse("patient-detail", kwargs={"external_id": patient_id}) after removing the
"deceased_datetime" key from the payload (del patient_data["deceased_datetime"])
and assert status 200 and that response.data["deceased_datetime"] is still not
None to ensure partial updates don't clear the field.

@codecov
Copy link

codecov bot commented Feb 23, 2026

Codecov Report

❌ Patch coverage is 83.33333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 75.35%. Comparing base (3c30dc8) to head (f57cbc3).

Files with missing lines Patch % Lines
care/emr/resources/patient/spec.py 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3542      +/-   ##
===========================================
+ Coverage    75.32%   75.35%   +0.02%     
===========================================
  Files          473      473              
  Lines        22066    22072       +6     
  Branches      2305     2307       +2     
===========================================
+ Hits         16622    16633      +11     
+ Misses        4926     4922       -4     
+ Partials       518      517       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
care/emr/tests/test_patient_api.py (1)

308-317: ⚠️ Potential issue | 🔴 Critical

geo_organization is referenced before assignment, causing test failure.

The initial block in test_invalid_age_and_death_date uses geo_organization before it is defined (F821), so the test crashes before reaching the intended validation checks.

🛠️ Minimal fix
     def test_invalid_age_and_death_date(self):
-        patient_data = self.generate_patient_data(
-            geo_organization=geo_organization.external_id
-        )
-        response = self.client.post(self.base_url, patient_data, format="json")
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        patient_id = response.data["id"]
-        delete_url = reverse("patient-detail", kwargs={"external_id": patient_id})
-        response = self.client.delete(delete_url)
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         user = self.create_user()
         geo_organization = self.create_organization(org_type="govt")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/tests/test_patient_api.py` around lines 308 - 317,
test_invalid_age_and_death_date references geo_organization before it’s defined;
fix by creating or assigning geo_organization before calling
generate_patient_data: e.g. set geo_organization =
self.create_geo_organization() or obtain it from the test setup/fixture, then
call patient_data =
self.generate_patient_data(geo_organization=geo_organization.external_id); keep
the rest of the test (base_url, reverse("patient-detail"), delete) unchanged.
🧹 Nitpick comments (1)
care/emr/tests/test_patient_api.py (1)

291-307: Test name no longer matches what it asserts.

test_delete_patient_as_non_superuser now verifies a PUT unmark-deceased permission path, not delete authorization. Please rename it to match intent, or restore delete assertions in a separate test.

As per coding guidelines **/*.py: Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance). Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/tests/test_patient_api.py` around lines 291 - 307, The test
currently named test_delete_patient_as_non_superuser is asserting that a
non-superuser cannot unset deceased_datetime via a PUT, so rename the test to
reflect its intent (e.g., test_unmark_deceased_as_non_superuser) or separate
into two tests (one for delete authorization and one for unmarking deceased) and
update any references; ensure the new test name uses lowercase_with_underscores
and is descriptive, and keep the existing assertions around the PUT to verify
HTTP_403_FORBIDDEN on attempting to set deceased_datetime to None in the
patient-detail update flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@care/emr/tests/test_patient_api.py`:
- Around line 241-244: The test test_non_superuser_cannot_unmark_deceased is a
stub that leaves unused locals and should either be implemented or removed; to
fix it, implement the test: use create_organization and create_user to set up a
non-superuser, create a patient record with deceased_datetime set, attempt to
unmark the patient (e.g., call the API endpoint or PatientViewSet method used
elsewhere in tests) as the non-superuser, assert the response is forbidden (HTTP
403) and verify the patient's deceased_datetime remains unchanged; if this
behavior is already covered by another test, simply delete the stub to remove
the unused-variable lint error.

---

Outside diff comments:
In `@care/emr/tests/test_patient_api.py`:
- Around line 308-317: test_invalid_age_and_death_date references
geo_organization before it’s defined; fix by creating or assigning
geo_organization before calling generate_patient_data: e.g. set geo_organization
= self.create_geo_organization() or obtain it from the test setup/fixture, then
call patient_data =
self.generate_patient_data(geo_organization=geo_organization.external_id); keep
the rest of the test (base_url, reverse("patient-detail"), delete) unchanged.

---

Nitpick comments:
In `@care/emr/tests/test_patient_api.py`:
- Around line 291-307: The test currently named
test_delete_patient_as_non_superuser is asserting that a non-superuser cannot
unset deceased_datetime via a PUT, so rename the test to reflect its intent
(e.g., test_unmark_deceased_as_non_superuser) or separate into two tests (one
for delete authorization and one for unmarking deceased) and update any
references; ensure the new test name uses lowercase_with_underscores and is
descriptive, and keep the existing assertions around the PUT to verify
HTTP_403_FORBIDDEN on attempting to set deceased_datetime to None in the
patient-detail update flow.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f57cbc3 and 5c132fd.

📒 Files selected for processing (1)
  • care/emr/tests/test_patient_api.py

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@care/emr/tests/test_patient_api.py`:
- Around line 241-270: After asserting the 403 in
test_non_superuser_cannot_unmark_deceased, perform a GET on the patient-detail
(reuse update_url or reverse with external_id=patient_id) to fetch the current
record and assert the GET returns 200 and that
response.data["deceased_datetime"] is still set (either assertIsNotNone or
compare to the original deceased_time). This ensures the failed PUT did not
clear the deceased_datetime.
- Around line 209-239: Add a follow-up assertion in
test_non_superuser_cannot_unmark_deceased to ensure the patient's
deceased_datetime was not changed after the 403 response: after the existing
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) call, retrieve
the patient state (either via a GET to reverse("patient-detail",
kwargs={"external_id": patient_id}) or by reloading the patient record from the
DB using the patient_id) and assert that the returned data/model's
deceased_datetime is still not None; this ensures the test validates that no
partial update occurred.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c132fd and ba8b4fb.

📒 Files selected for processing (1)
  • care/emr/tests/test_patient_api.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mark Patient Undead

1 participant