This commit is contained in:
2026-05-05 00:56:05 +03:30
parent 21b734f6a7
commit cfe60f6729
85 changed files with 1786 additions and 3840 deletions
+6 -3
View File
@@ -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),
),
]
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,