Skip to content
49 changes: 49 additions & 0 deletions care/emr/api/viewsets/resource_category.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.db import transaction
from django_filters import rest_framework as filters
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
Expand All @@ -7,11 +8,15 @@
from care.emr.api.viewsets.base import (
EMRBaseViewSet,
EMRCreateMixin,
EMRDestroyMixin,
EMRListMixin,
EMRRetrieveMixin,
EMRUpdateMixin,
EMRUpsertMixin,
)
from care.emr.models.activity_definition import ActivityDefinition
from care.emr.models.charge_item_definition import ChargeItemDefinition
from care.emr.models.product_knowledge import ProductKnowledge
from care.emr.models.resource_category import (
ResourceCategory,
summarise_monetary_components,
Expand Down Expand Up @@ -48,6 +53,7 @@ class ResourceCategoryViewSet(
EMRListMixin,
EMRUpsertMixin,
EMRBaseViewSet,
EMRDestroyMixin,
):
lookup_field = "slug"
database_model = ResourceCategory
Expand Down Expand Up @@ -107,6 +113,46 @@ def perform_update(self, instance):
)
super().perform_update(instance)

def validate_destroy(self, instance):
if instance.children.exists():
raise ValidationError("Cannot delete a resource category with children")
resource_type = instance.resource_type
if (
(
resource_type
== ResourceCategoryResourceTypeOptions.activity_definition.value
and ActivityDefinition.objects.filter(category=instance).exists()
)
or (
resource_type
== ResourceCategoryResourceTypeOptions.charge_item_definition.value
and ChargeItemDefinition.objects.filter(category=instance).exists()
)
or (
resource_type
== ResourceCategoryResourceTypeOptions.product_knowledge.value
and ProductKnowledge.objects.filter(category=instance).exists()
)
):
raise ValidationError(
"Cannot delete a resource category associated with a resource"
)

def perform_destroy(self, instance):
parent = instance.parent
with transaction.atomic():
instance.deleted = True
instance.updated_by = self.request.user
instance.save(update_fields=["deleted", "updated_by", "modified_date"])
if (
parent
and not ResourceCategory.objects.filter(
parent=parent, deleted=False
).exists()
):
parent.has_children = False
parent.save(update_fields=["has_children", "modified_date"])

def authorize_create(self, instance):
if not AuthorizationController.call(
"can_write_facility_resource_category",
Expand All @@ -123,6 +169,9 @@ def authorize_update(self, request_obj, model_instance):
):
raise PermissionDenied("Access denied to resource category")

def authorize_destroy(self, instance):
self.authorize_update({}, instance)

def get_queryset(self):
queryset = super().get_queryset()
facility_obj = self.get_facility_obj()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 6.0 on 2026-07-06 07:53

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('emr', '0079_resourcerequest_extensions'),
]

operations = [
migrations.AlterField(
model_name='activitydefinition',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='emr.resourcecategory'),
),
migrations.AlterField(
model_name='chargeitemdefinition',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='emr.resourcecategory'),
),
migrations.AlterField(
model_name='productknowledge',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='emr.resourcecategory'),
),
]
2 changes: 1 addition & 1 deletion care/emr/models/activity_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ActivityDefinition(SlugBaseModel):
tags = ArrayField(models.IntegerField(), default=list)
category = models.ForeignKey(
"emr.ResourceCategory",
on_delete=models.CASCADE,
on_delete=models.PROTECT,
null=True,
blank=True,
)
2 changes: 1 addition & 1 deletion care/emr/models/charge_item_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ChargeItemDefinition(SlugBaseModel):
can_edit_charge_item = models.BooleanField(default=True)
category = models.ForeignKey(
"emr.ResourceCategory",
on_delete=models.CASCADE,
on_delete=models.PROTECT,
null=True,
blank=True,
)
Expand Down
2 changes: 1 addition & 1 deletion care/emr/models/product_knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ProductKnowledge(SlugBaseModel):
base_unit = models.JSONField(default=dict, null=True, blank=True)
category = models.ForeignKey(
"emr.ResourceCategory",
on_delete=models.CASCADE,
on_delete=models.PROTECT,
null=True,
blank=True,
)
Expand Down
154 changes: 154 additions & 0 deletions care/emr/tests/test_resource_category_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from django.urls import reverse
from model_bakery import baker

from care.emr.models.activity_definition import ActivityDefinition
from care.emr.models.charge_item_definition import ChargeItemDefinition
from care.emr.models.product_knowledge import ProductKnowledge
from care.emr.models.resource_category import ResourceCategory
from care.security.permissions.resource_category import ResourceCategoryPermissions
from care.utils.tests.base import CareAPITestBase
Expand Down Expand Up @@ -265,6 +268,28 @@ def test_list_resource_categories_with_parent_filter(self):
)
self.assertEqual(response.data["results"][0]["title"], "Child Category")

def test_list_resource_categories_with_parent_filter_no_results(self):
self.client.force_authenticate(user=self.super_user)
parent_category = self.create_resource_category(
slug_value="parent-category",
facility=self.facility,
title="Parent Category",
)
self.create_resource_category(
slug_value="child-category",
facility=self.facility,
title="Child Category",
parent=parent_category,
)
response = self.client.get(
self.get_url(self.facility) + "?parent=", format="json"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data["results"]), 1)
self.assertEqual(
response.data["results"][0]["id"], str(parent_category.external_id)
)

# Test cases for retrieve resource category api

def test_retrieve_resource_category_as_super_user(self):
Expand Down Expand Up @@ -537,3 +562,132 @@ def test_create_resource_category_with_duplicate_slug(self):
"Resource category with this slug already exists.",
status_code=400,
)

def test_delete_resource_category_as_super_user(self):
self.client.force_authenticate(user=self.super_user)
resource_category = self.create_resource_category(
slug_value="test-category", facility=self.facility
)
url = self.get_detail_url(self.facility, resource_category)
response = self.client.delete(url, format="json")
self.assertEqual(response.status_code, 204)
self.assertFalse(
ResourceCategory.objects.filter(id=resource_category.id).exists()
)

def test_delete_resource_category_as_regular_user_with_permission(self):
self.attach_role_facility_organization_user(
self.facility_organization, self.user, self.role
)
self.client.force_authenticate(user=self.user)
resource_category = self.create_resource_category(
slug_value="test-category", facility=self.facility
)
url = self.get_detail_url(self.facility, resource_category)
response = self.client.delete(url, format="json")
self.assertEqual(response.status_code, 204)
self.assertFalse(
ResourceCategory.objects.filter(id=resource_category.id).exists()
)

def test_delete_resource_category_as_regular_user_without_permission(self):
self.client.force_authenticate(user=self.user)
resource_category = self.create_resource_category(
slug_value="test-category", facility=self.facility
)
url = self.get_detail_url(self.facility, resource_category)
response = self.client.delete(url, format="json")
self.assertEqual(response.status_code, 403)
self.assertEqual(response.data["detail"], "Access denied to resource category")
self.assertTrue(
ResourceCategory.objects.filter(id=resource_category.id).exists()
)

def test_delete_resource_category_with_associated_product_knowledge(self):
self.client.force_authenticate(user=self.super_user)
resource_category = self.create_resource_category(
slug_value="test-category",
facility=self.facility,
resource_type="product_knowledge",
)
# Create a ProductKnowledge associated with the resource category
baker.make(
ProductKnowledge,
facility=self.facility,
name="Test Product",
slug=self.calculate_slug("test-product", self.facility),
category=resource_category,
)
url = self.get_detail_url(self.facility, resource_category)
response = self.client.delete(url, format="json")
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data["errors"][0]["msg"],
"Cannot delete a resource category associated with a resource",
)

def test_delete_resource_category_with_associated_activity_definition(self):
self.client.force_authenticate(user=self.super_user)
resource_category = self.create_resource_category(
slug_value="test-category",
facility=self.facility,
resource_type="activity_definition",
)
# Create an ActivityDefinition associated with the resource category
baker.make(
ActivityDefinition,
title="Test Activity",
facility=self.facility,
slug=self.calculate_slug("test-activity", self.facility),
category=resource_category,
)
url = self.get_detail_url(self.facility, resource_category)
response = self.client.delete(url, format="json")
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data["errors"][0]["msg"],
"Cannot delete a resource category associated with a resource",
)

def test_delete_resource_category_with_associated_charge_item_definition(self):
self.client.force_authenticate(user=self.super_user)
resource_category = self.create_resource_category(
slug_value="test-category",
facility=self.facility,
resource_type="charge_item_definition",
)
baker.make(
ChargeItemDefinition,
facility=self.facility,
title="Test Charge Item",
slug=self.calculate_slug("test-charge-item", self.facility),
category=resource_category,
)
url = self.get_detail_url(self.facility, resource_category)
response = self.client.delete(url, format="json")
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data["errors"][0]["msg"],
"Cannot delete a resource category associated with a resource",
)

def test_delete_resource_category_with_children(self):
self.client.force_authenticate(user=self.super_user)
parent_category = self.create_resource_category(
slug_value="parent-category",
facility=self.facility,
title="Parent Category",
)
self.create_resource_category(
slug_value="child-category",
facility=self.facility,
title="Child Category",
parent=parent_category,
)
url = self.get_detail_url(self.facility, parent_category)
response = self.client.delete(url, format="json")
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data["errors"][0]["msg"],
"Cannot delete a resource category with children",
)
Loading