This commit is contained in:
2026-05-02 16:40:47 +03:30
parent 9c37e98b33
commit 21b734f6a7
5 changed files with 60 additions and 65 deletions
+4 -6
View File
@@ -64,15 +64,13 @@ class CropSimulationRequestSerializer(serializers.Serializer):
initial="11111111-1111-1111-1111-111111111111",
help_text="UUID مزرعه برای اجرای شبیه‌سازی.",
)
irrigation_plan_id = serializers.IntegerField(
irrigation_plan_uuid = serializers.UUIDField(
required=False,
min_value=1,
help_text="شناسه داخلی برنامه آبیاری برای ارسال context به AI.",
help_text="UUID برنامه آبیاری برای ارسال context به AI.",
)
fertilization_plan_id = serializers.IntegerField(
fertilization_plan_uuid = serializers.UUIDField(
required=False,
min_value=1,
help_text="شناسه داخلی برنامه کودی برای ارسال context به AI.",
help_text="UUID برنامه کودی برای ارسال context به AI.",
)
+10 -8
View File
@@ -395,13 +395,14 @@ class CropSimulationViewTests(TestCase):
response = self.api_client.post(
"/api/yield-harvest/current-farm-chart/",
{"farm_uuid": str(self.farm.farm_uuid), "irrigation_plan_id": irrigation_plan.id},
{"farm_uuid": str(self.farm.farm_uuid), "irrigation_plan_uuid": str(irrigation_plan.uuid)},
format="json",
)
self.assertEqual(response.status_code, 200)
sent_payload = mock_external_api_request.call_args.kwargs["payload"]
self.assertEqual(sent_payload["irrigation_plan"]["id"], irrigation_plan.id)
self.assertEqual(sent_payload["irrigation_plan"]["uuid"], str(irrigation_plan.uuid))
@patch("yield_harvest.views.external_api_request")
def test_harvest_prediction_includes_selected_plans(self, mock_external_api_request):
@@ -418,13 +419,14 @@ class CropSimulationViewTests(TestCase):
response = self.api_client.post(
"/api/yield-harvest/harvest-prediction/",
{"farm_uuid": str(self.farm.farm_uuid), "fertilization_plan_id": fertilization_plan.id},
{"farm_uuid": str(self.farm.farm_uuid), "fertilization_plan_uuid": str(fertilization_plan.uuid)},
format="json",
)
self.assertEqual(response.status_code, 200)
sent_payload = mock_external_api_request.call_args.kwargs["payload"]
self.assertEqual(sent_payload["fertilization_plan"]["id"], fertilization_plan.id)
self.assertEqual(sent_payload["fertilization_plan"]["uuid"], str(fertilization_plan.uuid))
@patch("yield_harvest.views.external_api_request")
def test_yield_prediction_proxies_to_ai_service(self, mock_external_api_request):
@@ -549,8 +551,8 @@ class CropSimulationViewTests(TestCase):
"/api/yield-harvest/yield-prediction/",
{
"farm_uuid": str(self.farm.farm_uuid),
"irrigation_plan_id": irrigation_plan.id,
"fertilization_plan_id": fertilization_plan.id,
"irrigation_plan_uuid": str(irrigation_plan.uuid),
"fertilization_plan_uuid": str(fertilization_plan.uuid),
},
format="json",
)
@@ -565,7 +567,7 @@ class CropSimulationViewTests(TestCase):
"npk-202020",
)
def test_yield_prediction_rejects_foreign_plan_ids(self):
def test_yield_prediction_rejects_foreign_plan_uuids(self):
other_irrigation_plan = IrrigationPlan.objects.create(
farm=self.other_farm,
source=IrrigationPlan.SOURCE_FREE_TEXT,
@@ -576,13 +578,13 @@ class CropSimulationViewTests(TestCase):
"/api/yield-harvest/yield-prediction/",
{
"farm_uuid": str(self.farm.farm_uuid),
"irrigation_plan_id": other_irrigation_plan.id,
"irrigation_plan_uuid": str(other_irrigation_plan.uuid),
},
format="json",
)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json()["data"]["irrigation_plan_id"][0], "Irrigation plan not found.")
self.assertEqual(response.json()["data"]["irrigation_plan_uuid"][0], "Irrigation plan not found.")
@patch("yield_harvest.views.external_api_request")
def test_yield_harvest_summary_top_level_route_proxies_to_ai_service(self, mock_external_api_request):
@@ -654,7 +656,7 @@ class CropSimulationViewTests(TestCase):
)
response = self.api_client.get(
f"/api/yield-harvest/yield-harvest-summary/?farm_uuid={self.farm.farm_uuid}&irrigation_plan_id={irrigation_plan.id}&fertilization_plan_id={fertilization_plan.id}"
f"/api/yield-harvest/yield-harvest-summary/?farm_uuid={self.farm.farm_uuid}&irrigation_plan_uuid={irrigation_plan.uuid}&fertilization_plan_uuid={fertilization_plan.uuid}"
)
self.assertEqual(response.status_code, 200)
+30 -35
View File
@@ -79,16 +79,16 @@ class YieldHarvestSummaryView(APIView):
if error_response is not None:
return error_response
irrigation_plan_id, irrigation_plan_error = CropSimulationBaseView._parse_optional_plan_id(
request.query_params.get("irrigation_plan_id"),
"irrigation_plan_id",
irrigation_plan_uuid, irrigation_plan_error = CropSimulationBaseView._parse_optional_plan_uuid(
request.query_params.get("irrigation_plan_uuid"),
"irrigation_plan_uuid",
)
if irrigation_plan_error is not None:
return irrigation_plan_error
fertilization_plan_id, fertilization_plan_error = CropSimulationBaseView._parse_optional_plan_id(
request.query_params.get("fertilization_plan_id"),
"fertilization_plan_id",
fertilization_plan_uuid, fertilization_plan_error = CropSimulationBaseView._parse_optional_plan_uuid(
request.query_params.get("fertilization_plan_uuid"),
"fertilization_plan_uuid",
)
if fertilization_plan_error is not None:
return fertilization_plan_error
@@ -101,10 +101,10 @@ class YieldHarvestSummaryView(APIView):
if request.query_params.get("include_narrative") is not None:
query["include_narrative"] = request.query_params.get("include_narrative")
ai_payload, plan_error = self._build_ai_payload_with_selected_plans(
ai_payload, plan_error = CropSimulationBaseView()._build_ai_payload_with_selected_plans(
farm,
irrigation_plan_id=irrigation_plan_id,
fertilization_plan_id=fertilization_plan_id,
irrigation_plan_uuid=irrigation_plan_uuid,
fertilization_plan_uuid=fertilization_plan_uuid,
)
if plan_error is not None:
return plan_error
@@ -217,35 +217,35 @@ class CropSimulationBaseView(APIView):
return ""
@staticmethod
def _get_irrigation_plan_or_error(farm, plan_id):
if not plan_id:
def _get_irrigation_plan_or_error(farm, plan_uuid):
if not plan_uuid:
return None, None
plan = IrrigationPlan.objects.filter(
id=plan_id,
uuid=plan_uuid,
farm=farm,
is_deleted=False,
).first()
if plan is None:
return None, Response(
{"code": 404, "msg": "error", "data": {"irrigation_plan_id": ["Irrigation plan not found."]}},
{"code": 404, "msg": "error", "data": {"irrigation_plan_uuid": ["Irrigation plan not found."]}},
status=status.HTTP_404_NOT_FOUND,
)
return plan, None
@staticmethod
def _get_fertilization_plan_or_error(farm, plan_id):
if not plan_id:
def _get_fertilization_plan_or_error(farm, plan_uuid):
if not plan_uuid:
return None, None
plan = FertilizationPlan.objects.filter(
id=plan_id,
uuid=plan_uuid,
farm=farm,
is_deleted=False,
).first()
if plan is None:
return None, Response(
{"code": 404, "msg": "error", "data": {"fertilization_plan_id": ["Fertilization plan not found."]}},
{"code": 404, "msg": "error", "data": {"fertilization_plan_uuid": ["Fertilization plan not found."]}},
status=status.HTTP_404_NOT_FOUND,
)
return plan, None
@@ -268,13 +268,13 @@ class CropSimulationBaseView(APIView):
"response_payload": plan.response_payload if isinstance(plan.response_payload, dict) else {},
}
def _build_ai_payload_with_selected_plans(self, farm, irrigation_plan_id=None, fertilization_plan_id=None):
irrigation_plan, irrigation_error = self._get_irrigation_plan_or_error(farm, irrigation_plan_id)
def _build_ai_payload_with_selected_plans(self, farm, irrigation_plan_uuid=None, fertilization_plan_uuid=None):
irrigation_plan, irrigation_error = self._get_irrigation_plan_or_error(farm, irrigation_plan_uuid)
if irrigation_error is not None:
return None, irrigation_error
fertilization_plan, fertilization_error = self._get_fertilization_plan_or_error(
farm, fertilization_plan_id
farm, fertilization_plan_uuid
)
if fertilization_error is not None:
return None, fertilization_error
@@ -291,19 +291,14 @@ class CropSimulationBaseView(APIView):
return ai_payload, None
@staticmethod
def _parse_optional_plan_id(raw_value, field_name):
def _parse_optional_plan_uuid(raw_value, field_name):
if raw_value in (None, ""):
return None, None
try:
parsed_value = int(raw_value)
except (TypeError, ValueError):
parsed_value = str(serializers.UUIDField().to_internal_value(raw_value))
except serializers.ValidationError:
return None, Response(
{"code": 400, "msg": "error", "data": {field_name: ["A valid integer is required."]}},
status=status.HTTP_400_BAD_REQUEST,
)
if parsed_value < 1:
return None, Response(
{"code": 400, "msg": "error", "data": {field_name: ["Ensure this value is greater than or equal to 1."]}},
{"code": 400, "msg": "error", "data": {field_name: ["Must be a valid UUID."]}},
status=status.HTTP_400_BAD_REQUEST,
)
return parsed_value, None
@@ -328,8 +323,8 @@ class CurrentFarmChartView(CropSimulationBaseView):
ai_payload, plan_error = self._build_ai_payload_with_selected_plans(
farm,
irrigation_plan_id=payload.get("irrigation_plan_id"),
fertilization_plan_id=payload.get("fertilization_plan_id"),
irrigation_plan_uuid=payload.get("irrigation_plan_uuid"),
fertilization_plan_uuid=payload.get("fertilization_plan_uuid"),
)
if plan_error is not None:
return plan_error
@@ -363,8 +358,8 @@ class HarvestPredictionView(CropSimulationBaseView):
ai_payload, plan_error = self._build_ai_payload_with_selected_plans(
farm,
irrigation_plan_id=payload.get("irrigation_plan_id"),
fertilization_plan_id=payload.get("fertilization_plan_id"),
irrigation_plan_uuid=payload.get("irrigation_plan_uuid"),
fertilization_plan_uuid=payload.get("fertilization_plan_uuid"),
)
if plan_error is not None:
return plan_error
@@ -398,8 +393,8 @@ class YieldPredictionView(CropSimulationBaseView):
ai_payload, plan_error = self._build_ai_payload_with_selected_plans(
farm,
irrigation_plan_id=payload.get("irrigation_plan_id"),
fertilization_plan_id=payload.get("fertilization_plan_id"),
irrigation_plan_uuid=payload.get("irrigation_plan_uuid"),
fertilization_plan_uuid=payload.get("fertilization_plan_uuid"),
)
if plan_error is not None:
return plan_error