Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
parent.has_children = ResourceCategory.objects.filter(
parent=parent
).exists()
parent.updated_by = self.request.user
parent.save(
update_fields=["has_children", "updated_by", "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