diff --git a/src/planscape/impacts/permissions.py b/src/planscape/impacts/permissions.py index bdf7c7653..4f7e16fb2 100644 --- a/src/planscape/impacts/permissions.py +++ b/src/planscape/impacts/permissions.py @@ -114,6 +114,8 @@ def has_permission(self, request, view): match view.action: case "create": return TreatmentPlanPermission.can_add_scenario(request.user, tx_plan) + case "batch_delete": + return TreatmentPlanPermission.can_remove(request.user, tx_plan) case _: return TreatmentPlanPermission.can_view(request.user, tx_plan) diff --git a/src/planscape/impacts/serializers.py b/src/planscape/impacts/serializers.py index 79a59c180..972fab0ab 100644 --- a/src/planscape/impacts/serializers.py +++ b/src/planscape/impacts/serializers.py @@ -138,6 +138,12 @@ class Meta: ) +class TreatmentPrescriptionBatchDeleteSerializer(serializers.Serializer): + stand_ids = serializers.ListField( + child=serializers.IntegerField(), allow_empty=False + ) + + class SummarySerializer(serializers.Serializer): project_area = serializers.PrimaryKeyRelatedField( queryset=ProjectArea.objects.all(), diff --git a/src/planscape/impacts/tests/test_views.py b/src/planscape/impacts/tests/test_views.py index bd1a2f5c1..59efc653f 100644 --- a/src/planscape/impacts/tests/test_views.py +++ b/src/planscape/impacts/tests/test_views.py @@ -6,7 +6,10 @@ from collaboration.models import Permissions, Role, UserObjectRole from collaboration.services import get_content_type from impacts.models import TreatmentPlan -from impacts.tests.factories import TreatmentPlanFactory, TreatmentPrescriptionFactory +from impacts.tests.factories import ( + TreatmentPlanFactory, + TreatmentPrescriptionFactory, +) from planning.tests.factories import ScenarioFactory from planscape.tests.factories import UserFactory @@ -205,3 +208,88 @@ def test_list_tx_rx(self): data = response.json() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(data["count"], 2) + + +class TxPrescriptionBatchDeleteTest(APITransactionTestCase): + def setUp(self): + self.tx_plan = TreatmentPlanFactory.create() + self.alt_tx_plan = TreatmentPlanFactory.create() + self.client.force_authenticate(user=self.tx_plan.scenario.user) + self.txrx_owned_list = TreatmentPrescriptionFactory.create_batch( + 10, treatment_plan=self.tx_plan + ) + # plans for a different user + self.txrx_other_list = TreatmentPrescriptionFactory.create_batch( + 10, treatment_plan=self.alt_tx_plan + ) + + def test_batch_delete_tx_rx(self): + payload = {"stand_ids": [txrx.stand_id for txrx in self.txrx_owned_list]} + response = self.client.post( + reverse( + "api:impacts:tx-prescriptions-delete-prescriptions", + kwargs={"tx_plan_pk": self.tx_plan.pk}, + ), + data=payload, + format="json", + ) + response_data = response.json() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response_data["result"][0], 10) + + def test_batch_delete_tx_rx_bad_values(self): + payload = {"stand_ids": [None]} + response = self.client.post( + reverse( + "api:impacts:tx-prescriptions-delete-prescriptions", + kwargs={"tx_plan_pk": self.tx_plan.pk}, + ), + data=payload, + format="json", + ) + response_data = response.json() + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_batch_delete_tx_rx_empty_list(self): + payload = {"stand_ids": []} + response = self.client.post( + reverse( + "api:impacts:tx-prescriptions-delete-prescriptions", + kwargs={"tx_plan_pk": self.tx_plan.pk}, + ), + data=payload, + format="json", + ) + response_data = response.json() + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_batch_delete_nonowned_tx_rx(self): + payload = {"stand_ids": [txrx.stand_id for txrx in self.txrx_other_list]} + response = self.client.post( + reverse( + "api:impacts:tx-prescriptions-delete-prescriptions", + kwargs={"tx_plan_pk": self.alt_tx_plan.pk}, + ), + data=payload, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Testing a request to delete various stand_ids for some txrx records don't match the treatment plan id + # current behavior is to only delete items matching the given tx_plan and quietly ignore the rest + def test_batch_delete_mixed_tx_rx(self): + owned_ids = [txrx.stand_id for txrx in self.txrx_owned_list] + other_ids = [txrx.stand_id for txrx in self.txrx_other_list] + + payload = {"stand_ids": owned_ids + other_ids} + response = self.client.post( + reverse( + "api:impacts:tx-prescriptions-delete-prescriptions", + kwargs={"tx_plan_pk": self.tx_plan.pk}, + ), + data=payload, + format="json", + ) + response_data = response.json() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response_data["result"][0], 10) diff --git a/src/planscape/impacts/views.py b/src/planscape/impacts/views.py index 6f7bcf8c7..c16f59769 100644 --- a/src/planscape/impacts/views.py +++ b/src/planscape/impacts/views.py @@ -19,6 +19,7 @@ TreatmentPlanSerializer, TreatmentPrescriptionSerializer, TreatmentPrescriptionListSerializer, + TreatmentPrescriptionBatchDeleteSerializer, UpsertTreamentPrescriptionSerializer, ) from impacts.services import ( @@ -191,3 +192,20 @@ def create(self, request, *args, **kwargs): def perform_create(self, serializer): return upsert_treatment_prescriptions(**serializer.validated_data) + + @action(detail=False, methods=["post"]) + def delete_prescriptions(self, request, tx_plan_pk=None): + serializer = TreatmentPrescriptionBatchDeleteSerializer(data=request.data) + + if not serializer.is_valid(): + return response.Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + stand_ids = serializer.validated_data.get("stand_ids", []) + + delete_result = TreatmentPrescription.objects.filter( + stand_id__in=stand_ids, treatment_plan_id=tx_plan_pk + ).delete() + + return response.Response({"result": delete_result}, status=status.HTTP_200_OK)