UPDATE
This commit is contained in:
@@ -42,7 +42,7 @@ GET /api/irrigation/plans/?farm_uuid=11111111-1111-1111-1111-111111111111&page=1
|
||||
"crop_id": "گندم",
|
||||
"plant_name": "گندم",
|
||||
"growth_stage": "flowering",
|
||||
"is_active": true,
|
||||
"is_active": false,
|
||||
"created_at": "2025-02-24T10:20:30Z"
|
||||
}
|
||||
],
|
||||
@@ -63,6 +63,7 @@ GET /api/irrigation/plans/?farm_uuid=11111111-1111-1111-1111-111111111111&page=1
|
||||
|
||||
- فقط planهایی برگردانده میشوند که `is_deleted=False` باشند.
|
||||
- ترتیب لیست از جدید به قدیم است.
|
||||
- در هر مزرعه، در هر نوع plan فقط یک plan میتواند `is_active=true` باشد.
|
||||
|
||||
---
|
||||
|
||||
@@ -95,7 +96,7 @@ GET /api/irrigation/plans/6d6a1f0d-1a9b-4f2f-8fe1-2d73d9d2d9f1/
|
||||
"crop_id": "گندم",
|
||||
"plant_name": "گندم",
|
||||
"growth_stage": "flowering",
|
||||
"is_active": true,
|
||||
"is_active": false,
|
||||
"created_at": "2025-02-24T10:20:30Z",
|
||||
"updated_at": "2025-02-24T10:20:30Z",
|
||||
"plan_payload": {
|
||||
@@ -195,7 +196,7 @@ Content-Type: application/json
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"plan_uuid": "6d6a1f0d-1a9b-4f2f-8fe1-2d73d9d2d9f1",
|
||||
"is_active": false
|
||||
"is_active": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -223,6 +224,8 @@ Content-Type: application/json
|
||||
|
||||
## Summary
|
||||
|
||||
- planهای جدید بهصورت پیشفرض `inactive` ساخته میشوند.
|
||||
- در هر مزرعه فقط یک plan از این نوع میتواند `active` باشد.
|
||||
- `GET /api/irrigation/plans/` لیست برنامهها
|
||||
- `GET /api/irrigation/plans/{plan_uuid}/` جزئیات برنامه
|
||||
- `DELETE /api/irrigation/plans/{plan_uuid}/` حذف نرم برنامه
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("irrigation_recommendation", "0003_irrigationplan"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="irrigationplan",
|
||||
name="is_active",
|
||||
field=models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
]
|
||||
@@ -74,7 +74,7 @@ class IrrigationPlan(models.Model):
|
||||
plan_payload = models.JSONField(default=dict, blank=True)
|
||||
request_payload = models.JSONField(default=dict, blank=True)
|
||||
response_payload = models.JSONField(default=dict, blank=True)
|
||||
is_active = models.BooleanField(default=True, db_index=True)
|
||||
is_active = models.BooleanField(default=False, db_index=True)
|
||||
is_deleted = models.BooleanField(default=False, db_index=True)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
+71
-3
@@ -6,6 +6,7 @@ from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
|
||||
from external_api_adapter.adapter import AdapterResponse
|
||||
from farm_hub.models import FarmHub, FarmType
|
||||
from farmer_calendar.models import FarmerCalendarEvent
|
||||
|
||||
from .models import IrrigationPlan, IrrigationRecommendationRequest
|
||||
from .views import (
|
||||
@@ -312,7 +313,7 @@ class RecommendViewTests(TestCase):
|
||||
self.assertEqual(IrrigationPlan.objects.count(), 1)
|
||||
plan = IrrigationPlan.objects.get()
|
||||
self.assertEqual(plan.source, IrrigationPlan.SOURCE_RECOMMENDATION)
|
||||
self.assertTrue(plan.is_active)
|
||||
self.assertFalse(plan.is_active)
|
||||
self.assertFalse(plan.is_deleted)
|
||||
self.assertEqual(response.data["data"]["plan"]["durationMinutes"], 38)
|
||||
self.assertEqual(response.data["data"]["water_balance"]["active_kc"], 0.93)
|
||||
@@ -570,7 +571,7 @@ class IrrigationPlanApiTests(TestCase):
|
||||
def test_plan_status_patch_updates_is_active(self):
|
||||
request = self.factory.patch(
|
||||
f"/api/irrigation/plans/{self.plan.uuid}/status/",
|
||||
{"is_active": False},
|
||||
{"is_active": True},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
@@ -579,4 +580,71 @@ class IrrigationPlanApiTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.plan.refresh_from_db()
|
||||
self.assertFalse(self.plan.is_active)
|
||||
self.assertTrue(self.plan.is_active)
|
||||
|
||||
def test_activating_one_plan_deactivates_other_active_plan(self):
|
||||
other_plan = IrrigationPlan.objects.create(
|
||||
farm=self.farm,
|
||||
source=IrrigationPlan.SOURCE_FREE_TEXT,
|
||||
title="برنامه دوم",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
request = self.factory.patch(
|
||||
f"/api/irrigation/plans/{self.plan.uuid}/status/",
|
||||
{"is_active": True},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = IrrigationPlanStatusView.as_view()(request, plan_uuid=self.plan.uuid)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.plan.refresh_from_db()
|
||||
other_plan.refresh_from_db()
|
||||
self.assertTrue(self.plan.is_active)
|
||||
self.assertFalse(other_plan.is_active)
|
||||
|
||||
def test_plan_status_patch_syncs_calendar_events(self):
|
||||
self.plan.plan_payload = {
|
||||
"plan": {"durationMinutes": 25, "bestTimeOfDay": "05:30 - 06:00"},
|
||||
"water_balance": {
|
||||
"daily": [
|
||||
{
|
||||
"forecast_date": "2025-02-12",
|
||||
"gross_irrigation_mm": 17,
|
||||
"irrigation_timing": "05:30 - 06:00",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
self.plan.is_active = False
|
||||
self.plan.save(update_fields=["plan_payload", "is_active", "updated_at"])
|
||||
|
||||
activate_request = self.factory.patch(
|
||||
f"/api/irrigation/plans/{self.plan.uuid}/status/",
|
||||
{"is_active": True},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(activate_request, user=self.user)
|
||||
|
||||
activate_response = IrrigationPlanStatusView.as_view()(activate_request, plan_uuid=self.plan.uuid)
|
||||
|
||||
self.assertEqual(activate_response.status_code, 200)
|
||||
events = FarmerCalendarEvent.objects.filter(farm=self.farm, extended_props__plan_uuid=str(self.plan.uuid))
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events.first().extended_props["plan_type"], "irrigation")
|
||||
|
||||
deactivate_request = self.factory.patch(
|
||||
f"/api/irrigation/plans/{self.plan.uuid}/status/",
|
||||
{"is_active": False},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(deactivate_request, user=self.user)
|
||||
|
||||
deactivate_response = IrrigationPlanStatusView.as_view()(deactivate_request, plan_uuid=self.plan.uuid)
|
||||
|
||||
self.assertEqual(deactivate_response.status_code, 200)
|
||||
self.assertFalse(
|
||||
FarmerCalendarEvent.objects.filter(farm=self.farm, extended_props__plan_uuid=str(self.plan.uuid)).exists()
|
||||
)
|
||||
|
||||
+19
-3
@@ -15,6 +15,7 @@ from drf_spectacular.utils import extend_schema
|
||||
from config.swagger import code_response, status_response
|
||||
from external_api_adapter import request as external_api_request
|
||||
from farm_hub.models import FarmHub
|
||||
from farmer_calendar import PLAN_TYPE_IRRIGATION, delete_plan_events, sync_plan_events
|
||||
from water.serializers import WaterStressIndexSerializer
|
||||
from water.views import WaterStressIndexView
|
||||
from .mock_data import CONFIG_RESPONSE_DATA
|
||||
@@ -171,7 +172,7 @@ class RecommendView(FarmAccessMixin, APIView):
|
||||
return " - ".join(parts) if parts else "برنامه آبیاری"
|
||||
|
||||
def _create_plan_from_recommendation(self, recommendation, recommendation_data):
|
||||
IrrigationPlan.objects.create(
|
||||
plan = IrrigationPlan.objects.create(
|
||||
farm=recommendation.farm,
|
||||
source=IrrigationPlan.SOURCE_RECOMMENDATION,
|
||||
recommendation=recommendation,
|
||||
@@ -182,6 +183,7 @@ class RecommendView(FarmAccessMixin, APIView):
|
||||
request_payload=recommendation.request_payload,
|
||||
response_payload=recommendation.response_payload,
|
||||
)
|
||||
sync_plan_events(plan, PLAN_TYPE_IRRIGATION)
|
||||
|
||||
@staticmethod
|
||||
def _enrich_ai_payload(payload, farm):
|
||||
@@ -451,7 +453,7 @@ class PlanFromTextView(FarmAccessMixin, APIView):
|
||||
|
||||
final_plan = self._extract_final_plan(response_data)
|
||||
if final_plan and farm_uuid:
|
||||
IrrigationPlan.objects.create(
|
||||
plan = IrrigationPlan.objects.create(
|
||||
farm=farm,
|
||||
source=IrrigationPlan.SOURCE_FREE_TEXT,
|
||||
title=self._build_free_text_plan_title(final_plan),
|
||||
@@ -466,6 +468,7 @@ class PlanFromTextView(FarmAccessMixin, APIView):
|
||||
request_payload=payload,
|
||||
response_payload=response_data,
|
||||
)
|
||||
sync_plan_events(plan, PLAN_TYPE_IRRIGATION)
|
||||
|
||||
return Response(
|
||||
{"code": 200, "msg": response_data.get("msg", "موفق"), "data": response_data.get("data", response_data)},
|
||||
@@ -533,6 +536,7 @@ class IrrigationPlanDetailView(APIView):
|
||||
return Response({"code": 404, "msg": "Plan not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
plan.soft_delete()
|
||||
delete_plan_events(farm=plan.farm, plan_type=PLAN_TYPE_IRRIGATION, plan_uuid=plan.uuid)
|
||||
return Response({"code": 200, "msg": "success", "data": {"plan_uuid": str(plan.uuid), "is_deleted": True}}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -554,8 +558,20 @@ class IrrigationPlanStatusView(APIView):
|
||||
if plan is None:
|
||||
return Response({"code": 404, "msg": "Plan not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
plan.is_active = serializer.validated_data["is_active"]
|
||||
new_is_active = serializer.validated_data["is_active"]
|
||||
if new_is_active:
|
||||
IrrigationPlan.objects.filter(
|
||||
farm=plan.farm,
|
||||
is_deleted=False,
|
||||
is_active=True,
|
||||
).exclude(pk=plan.pk).update(is_active=False)
|
||||
|
||||
plan.is_active = new_is_active
|
||||
plan.save(update_fields=["is_active", "updated_at"])
|
||||
if plan.is_active:
|
||||
sync_plan_events(plan, PLAN_TYPE_IRRIGATION)
|
||||
else:
|
||||
delete_plan_events(farm=plan.farm, plan_type=PLAN_TYPE_IRRIGATION, plan_uuid=plan.uuid)
|
||||
return Response(
|
||||
{"code": 200, "msg": "success", "data": {"plan_uuid": str(plan.uuid), "is_active": plan.is_active}},
|
||||
status=status.HTTP_200_OK,
|
||||
|
||||
Reference in New Issue
Block a user