This commit is contained in:
2026-04-27 00:40:59 +03:30
parent 2cd96ceec6
commit 64e67c282c
56 changed files with 3912 additions and 745 deletions
+111 -35
View File
@@ -1,40 +1,5 @@
from rest_framework import serializers
from .mock_data import CONFIG_SLIDERS_ONLY
START_ENVIRONMENT_KEYS = [
item["key"]
for item in CONFIG_SLIDERS_ONLY["sliders"]
if item["key"] != "growth_speed"
]
def _defaults_from_sliders():
return {
item["key"]: item["default_value"]
for item in CONFIG_SLIDERS_ONLY["sliders"]
}
START_REQUEST_EXAMPLE = {
"environment": {
key: value for key, value in _defaults_from_sliders().items() if key != "growth_speed"
},
"growth_speed": _defaults_from_sliders().get("growth_speed", 1.5),
}
START_REQUEST_EXAMPLE_STATIC = {
"environment": {
"light": 75,
"water": 65,
"soil_ph": 6.5,
},
"growth_speed": 1.5,
}
def success_response():
return {"status": "success"}
@@ -86,3 +51,114 @@ class YieldHarvestSummarySerializer(serializers.Serializer):
yield_prediction_card = YieldPredictionCardSerializer(required=False)
yield_prediction_chart = YieldPredictionChartSerializer(required=False)
harvest_prediction_card = HarvestPredictionCardSerializer(required=False)
class CropSimulationRequestSerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField(required=True, help_text="UUID مزرعه برای اجرای شبیه‌سازی.")
plant_name = serializers.CharField(required=False, allow_blank=True, default="", help_text="نام گیاه یا محصول.")
class GrowthSimulationRequestSerializer(serializers.Serializer):
plant_name = serializers.CharField(required=True, help_text="نام گیاه برای شروع شبیه‌سازی رشد.")
dynamic_parameters = serializers.ListField(
child=serializers.CharField(),
required=True,
allow_empty=False,
help_text="لیست پارامترهای دینامیک موردنیاز مانند DVS یا LAI.",
)
farm_uuid = serializers.UUIDField(required=False, allow_null=True, help_text="UUID مزرعه؛ در صورت نبود باید weather ارسال شود.")
weather = serializers.JSONField(required=False, help_text="آب‌وهوا به‌صورت object یا array.")
soil_parameters = serializers.DictField(required=False, help_text="پارامترهای خاک.")
site_parameters = serializers.DictField(required=False, help_text="پارامترهای سایت.")
crop_parameters = serializers.DictField(required=False, help_text="پارامترهای محصول.")
agromanagement = serializers.DictField(required=False, help_text="تنظیمات مدیریت زراعی.")
page_size = serializers.IntegerField(required=False, min_value=1, max_value=50, help_text="اندازه صفحه بین 1 تا 50.")
def validate(self, attrs):
if not attrs.get("farm_uuid") and attrs.get("weather") in (None, "", [], {}):
raise serializers.ValidationError("At least one of 'farm_uuid' or 'weather' must be provided.")
return attrs
class GrowthSimulationQueuedDataSerializer(serializers.Serializer):
task_id = serializers.CharField(required=False, allow_blank=True, help_text="شناسه تسک شبیه‌سازی رشد.")
status_url = serializers.CharField(required=False, allow_blank=True, help_text="آدرس بررسی وضعیت تسک.")
plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه شبیه‌سازی‌شده.")
class GrowthSimulationProgressSerializer(serializers.Serializer):
current = serializers.IntegerField(required=False, help_text="مرحله فعلی پیشرفت.")
total = serializers.IntegerField(required=False, help_text="تعداد کل مراحل.")
percent = serializers.FloatField(required=False, help_text="درصد پیشرفت.")
class GrowthSimulationPaginationSerializer(serializers.Serializer):
page = serializers.IntegerField(required=False, help_text="شماره صفحه فعلی.")
page_size = serializers.IntegerField(required=False, help_text="اندازه صفحه.")
total_items = serializers.IntegerField(required=False, help_text="تعداد کل آیتم‌ها.")
total_pages = serializers.IntegerField(required=False, help_text="تعداد کل صفحات.")
has_next = serializers.BooleanField(required=False, help_text="آیا صفحه بعدی وجود دارد.")
has_previous = serializers.BooleanField(required=False, help_text="آیا صفحه قبلی وجود دارد.")
class GrowthSimulationResultSerializer(serializers.Serializer):
plant_name = serializers.CharField(required=False, allow_blank=True)
dynamic_parameters = serializers.ListField(child=serializers.CharField(), required=False)
engine = serializers.CharField(required=False, allow_blank=True, allow_null=True)
model_name = serializers.CharField(required=False, allow_blank=True, allow_null=True)
scenario_id = serializers.IntegerField(required=False)
simulation_warning = serializers.CharField(required=False, allow_blank=True)
summary_metrics = serializers.DictField(required=False)
stage_timeline = serializers.ListField(child=serializers.DictField(), required=False)
stages_page = serializers.ListField(child=serializers.DictField(), required=False)
pagination = GrowthSimulationPaginationSerializer(required=False)
daily_records_count = serializers.IntegerField(required=False)
default_page_size = serializers.IntegerField(required=False)
class GrowthSimulationStatusDataSerializer(serializers.Serializer):
task_id = serializers.CharField(required=False, allow_blank=True)
status = serializers.CharField(required=False, allow_blank=True)
message = serializers.CharField(required=False, allow_blank=True)
progress = GrowthSimulationProgressSerializer(required=False)
result = GrowthSimulationResultSerializer(required=False)
error = serializers.CharField(required=False, allow_blank=True)
class CurrentFarmChartSerializer(serializers.Serializer):
farm_uuid = serializers.CharField(required=False, allow_blank=True, allow_null=True)
plant_name = serializers.CharField(required=False, allow_blank=True)
engine = serializers.CharField(required=False, allow_blank=True, allow_null=True)
model_name = serializers.CharField(required=False, allow_blank=True, allow_null=True)
scenario_id = serializers.IntegerField(required=False)
simulation_warning = serializers.CharField(required=False, allow_blank=True)
categories = serializers.ListField(child=serializers.CharField(), required=False)
series = serializers.DictField(required=False)
summary = serializers.DictField(required=False)
current_state = serializers.DictField(required=False)
metrics = serializers.DictField(required=False)
daily_output = serializers.DictField(required=False)
class HarvestPredictionSerializer(serializers.Serializer):
date = serializers.CharField(required=False, allow_blank=True)
dateFormatted = serializers.CharField(required=False, allow_blank=True)
daysUntil = serializers.IntegerField(required=False)
description = serializers.CharField(required=False, allow_blank=True)
optimalWindowStart = serializers.CharField(required=False, allow_blank=True)
optimalWindowEnd = serializers.CharField(required=False, allow_blank=True)
gddDetails = serializers.DictField(required=False)
class YieldPredictionSerializer(serializers.Serializer):
farm_uuid = serializers.CharField(required=False, allow_blank=True)
plant_name = serializers.CharField(required=False, allow_blank=True, allow_null=True)
predictedYieldTons = serializers.FloatField(required=False)
predictedYieldRaw = serializers.FloatField(required=False)
unit = serializers.CharField(required=False, allow_blank=True)
sourceUnit = serializers.CharField(required=False, allow_blank=True)
simulationEngine = serializers.CharField(required=False, allow_blank=True, allow_null=True)
simulationModel = serializers.CharField(required=False, allow_blank=True, allow_null=True)
scenarioId = serializers.IntegerField(required=False)
simulationWarning = serializers.CharField(required=False, allow_blank=True)
supportingMetrics = serializers.DictField(required=False)
+294
View File
@@ -0,0 +1,294 @@
import json
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework.test import APIRequestFactory, force_authenticate
from external_api_adapter.adapter import AdapterResponse
from farm_hub.models import FarmHub, FarmType
from .views import (
CurrentFarmChartView,
GrowthSimulationStatusView,
GrowthSimulationView,
HarvestPredictionView,
YieldPredictionView,
)
class CropSimulationViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="farmer",
password="secret123",
email="farmer@example.com",
phone_number="09120000000",
)
self.other_user = get_user_model().objects.create_user(
username="other-farmer",
password="secret123",
email="other@example.com",
phone_number="09120000001",
)
self.farm_type = FarmType.objects.create(name="زراعی")
self.farm = FarmHub.objects.create(owner=self.user, farm_type=self.farm_type, name="Farm 1")
self.other_farm = FarmHub.objects.create(owner=self.other_user, farm_type=self.farm_type, name="Farm 2")
@patch("yield_harvest.views.external_api_request")
def test_growth_queues_simulation_task(self, mock_external_api_request):
mock_external_api_request.return_value = AdapterResponse(
status_code=202,
data={
"data": {
"task_id": "growth-task-123",
"status_url": "/api/crop-simulation/growth/growth-task-123/status/",
"plant_name": "گوجه‌فرنگی",
}
},
)
request = self.factory.post(
"/api/yield-harvest/crop-simulation/growth/",
{"plant_name": "گوجه‌فرنگی", "dynamic_parameters": ["DVS", "LAI"], "farm_uuid": str(self.farm.farm_uuid)},
format="json",
)
response = GrowthSimulationView.as_view()(request)
self.assertEqual(response.status_code, 202)
self.assertEqual(response.data["code"], 202)
self.assertEqual(response.data["data"]["task_id"], "growth-task-123")
mock_external_api_request.assert_called_once_with(
"ai",
"/api/crop-simulation/growth/",
method="POST",
payload={
"plant_name": "گوجه‌فرنگی",
"dynamic_parameters": ["DVS", "LAI"],
"farm_uuid": str(self.farm.farm_uuid),
},
)
@patch("yield_harvest.views.external_api_request")
def test_growth_top_level_route_queues_simulation_task(self, mock_external_api_request):
mock_external_api_request.return_value = AdapterResponse(
status_code=202,
data={
"data": {
"task_id": "growth-task-123",
"status_url": "/api/crop-simulation/growth/growth-task-123/status/",
"plant_name": "گوجه‌فرنگی",
}
},
)
response = self.client.post(
"/api/crop-simulation/growth/",
data=json.dumps(
{
"plant_name": "گوجه‌فرنگی",
"dynamic_parameters": ["DVS", "LAI"],
"farm_uuid": str(self.farm.farm_uuid),
}
),
content_type="application/json",
)
self.assertEqual(response.status_code, 202)
self.assertEqual(response.json()["data"]["task_id"], "growth-task-123")
def test_growth_requires_farm_uuid_or_weather(self):
request = self.factory.post(
"/api/yield-harvest/crop-simulation/growth/",
{"plant_name": "گوجه‌فرنگی", "dynamic_parameters": ["DVS"]},
format="json",
)
response = GrowthSimulationView.as_view()(request)
self.assertEqual(response.status_code, 400)
@patch("yield_harvest.views.external_api_request")
def test_growth_status_proxies_to_ai_service(self, mock_external_api_request):
mock_external_api_request.return_value = AdapterResponse(
status_code=200,
data={
"data": {
"task_id": "growth-task-123",
"status": "SUCCESS",
"message": "done",
"progress": {},
"result": {
"plant_name": "گوجه‌فرنگی",
"dynamic_parameters": ["DVS", "LAI"],
"scenario_id": 1,
},
"error": "",
}
},
)
request = self.factory.get("/api/yield-harvest/crop-simulation/growth/growth-task-123/status/?page=1&page_size=10")
response = GrowthSimulationStatusView.as_view()(request, task_id="growth-task-123")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["code"], 200)
self.assertEqual(response.data["data"]["status"], "SUCCESS")
mock_external_api_request.assert_called_once_with(
"ai",
"/api/crop-simulation/growth/growth-task-123/status/",
method="GET",
query={"page": "1", "page_size": "10"},
)
@patch("yield_harvest.views.external_api_request")
def test_growth_status_top_level_route_proxies_to_ai_service(self, mock_external_api_request):
mock_external_api_request.return_value = AdapterResponse(
status_code=200,
data={
"data": {
"task_id": "growth-task-123",
"status": "SUCCESS",
"message": "done",
"progress": {},
"result": {
"plant_name": "گوجه‌فرنگی",
"dynamic_parameters": ["DVS", "LAI"],
"scenario_id": 1,
},
"error": "",
}
},
)
response = self.client.get("/api/crop-simulation/growth/growth-task-123/status/?page=1&page_size=10")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["data"]["status"], "SUCCESS")
def test_legacy_plant_simulator_routes_are_unavailable(self):
legacy_paths = [
"/api/yield-harvest/plant-simulator/config/",
"/api/yield-harvest/plant-simulator/environment/",
"/api/yield-harvest/plant-simulator/reset/",
"/api/yield-harvest/plant-simulator/start/",
"/api/yield-harvest/plant-simulator/state/",
"/api/yield-harvest/plant-simulator/stop/",
]
for path in legacy_paths:
response = self.client.get(path)
self.assertEqual(response.status_code, 404, path)
@patch("yield_harvest.views.external_api_request")
def test_current_farm_chart_proxies_to_ai_service(self, mock_external_api_request):
mock_external_api_request.return_value = AdapterResponse(
status_code=200,
data={
"data": {
"result": {
"farm_uuid": str(self.farm.farm_uuid),
"plant_name": "wheat",
"scenario_id": 1,
"categories": ["day1"],
"series": {"biomass": [1.2]},
}
}
},
)
request = self.factory.post(
"/api/yield-harvest/crop-simulation/current-farm-chart/",
{"farm_uuid": str(self.farm.farm_uuid), "plant_name": "wheat"},
format="json",
)
force_authenticate(request, user=self.user)
response = CurrentFarmChartView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["code"], 200)
self.assertEqual(response.data["data"]["scenario_id"], 1)
mock_external_api_request.assert_called_once_with(
"ai",
"/api/crop-simulation/current-farm-chart/",
method="POST",
payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": "wheat"},
)
@patch("yield_harvest.views.external_api_request")
def test_harvest_prediction_proxies_to_ai_service(self, mock_external_api_request):
mock_external_api_request.return_value = AdapterResponse(
status_code=200,
data={
"data": {
"result": {
"date": "2026-07-15",
"dateFormatted": "15 Jul 2026",
"daysUntil": 96,
"gddDetails": {"current": 800},
}
}
},
)
request = self.factory.post(
"/api/yield-harvest/crop-simulation/harvest-prediction/",
{"farm_uuid": str(self.farm.farm_uuid)},
format="json",
)
force_authenticate(request, user=self.user)
response = HarvestPredictionView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["data"]["daysUntil"], 96)
mock_external_api_request.assert_called_once_with(
"ai",
"/api/crop-simulation/harvest-prediction/",
method="POST",
payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": ""},
)
@patch("yield_harvest.views.external_api_request")
def test_yield_prediction_proxies_to_ai_service(self, mock_external_api_request):
mock_external_api_request.return_value = AdapterResponse(
status_code=200,
data={
"data": {
"result": {
"farm_uuid": str(self.farm.farm_uuid),
"predictedYieldTons": 8.4,
"scenarioId": 1,
}
}
},
)
request = self.factory.post(
"/api/yield-harvest/crop-simulation/yield-prediction/",
{"farm_uuid": str(self.farm.farm_uuid)},
format="json",
)
force_authenticate(request, user=self.user)
response = YieldPredictionView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["data"]["predictedYieldTons"], 8.4)
def test_crop_simulation_rejects_foreign_farm_uuid(self):
request = self.factory.post(
"/api/yield-harvest/crop-simulation/yield-prediction/",
{"farm_uuid": str(self.other_farm.farm_uuid)},
format="json",
)
force_authenticate(request, user=self.user)
response = YieldPredictionView.as_view()(request)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.data["code"], 404)
self.assertEqual(response.data["data"]["farm_uuid"][0], "Farm not found.")
+1 -25
View File
@@ -1,30 +1,6 @@
from django.urls import path
from .views import (
ConfigView,
EnvironmentView,
ResetView,
StartView,
StateView,
StopView,
YieldHarvestSummaryView,
)
ConfigView.__module__ = "plant_simulator.views"
EnvironmentView.__module__ = "plant_simulator.views"
ResetView.__module__ = "plant_simulator.views"
StartView.__module__ = "plant_simulator.views"
StateView.__module__ = "plant_simulator.views"
StopView.__module__ = "plant_simulator.views"
plant_simulator_urlpatterns = [
path("config/", ConfigView.as_view(), name="plant-simulator-config"),
path("state/", StateView.as_view(), name="plant-simulator-state"),
path("start/", StartView.as_view(), name="plant-simulator-start"),
path("stop/", StopView.as_view(), name="plant-simulator-stop"),
path("reset/", ResetView.as_view(), name="plant-simulator-reset"),
path("environment/", EnvironmentView.as_view(), name="plant-simulator-environment"),
]
from .views import YieldHarvestSummaryView
urlpatterns = [
path("summary/", YieldHarvestSummaryView.as_view(), name="yield-harvest-summary"),
+230 -71
View File
@@ -1,6 +1,4 @@
"""
Yield & Harvest Prediction and Plant Simulator API views.
"""
"""Yield & Harvest Prediction and Crop Simulation API views."""
from rest_framework import serializers, status
from rest_framework.response import Response
@@ -8,70 +6,20 @@ from rest_framework.views import APIView
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from config.swagger import status_response
from config.swagger import farm_uuid_query_param, status_response
from external_api_adapter import request as external_api_request
from farm_hub.models import FarmHub
from .mock_data import CONFIG_SLIDERS_ONLY, START_RESPONSE_DATA, STATE_RESPONSE_DATA
from .models import YieldHarvestPredictionLog
from .serializers import YieldHarvestSummarySerializer, success_response, success_with_data
class ConfigView(APIView):
@extend_schema(
tags=["Plant Simulator"],
responses={200: status_response("PlantSimulatorConfigResponse", data=serializers.JSONField())},
)
def get(self, request):
return Response(success_with_data(CONFIG_SLIDERS_ONLY), status=status.HTTP_200_OK)
class StateView(APIView):
@extend_schema(
tags=["Plant Simulator"],
responses={200: status_response("PlantSimulatorStateResponse", data=serializers.JSONField())},
)
def get(self, request):
return Response(success_with_data(STATE_RESPONSE_DATA), status=status.HTTP_200_OK)
class StartView(APIView):
@extend_schema(
tags=["Plant Simulator"],
request=OpenApiTypes.OBJECT,
responses={200: status_response("PlantSimulatorStartResponse", data=serializers.JSONField())},
)
def post(self, request):
return Response(success_with_data(START_RESPONSE_DATA), status=status.HTTP_200_OK)
class StopView(APIView):
@extend_schema(
tags=["Plant Simulator"],
request=OpenApiTypes.OBJECT,
responses={200: status_response("PlantSimulatorStopResponse")},
)
def post(self, request):
return Response(success_response(), status=status.HTTP_200_OK)
class ResetView(APIView):
@extend_schema(
tags=["Plant Simulator"],
request=OpenApiTypes.OBJECT,
responses={200: status_response("PlantSimulatorResetResponse")},
)
def post(self, request):
return Response(success_response(), status=status.HTTP_200_OK)
class EnvironmentView(APIView):
@extend_schema(
tags=["Plant Simulator"],
request=OpenApiTypes.OBJECT,
responses={200: status_response("PlantSimulatorEnvironmentResponse")},
)
def patch(self, request):
return Response(success_response(), status=status.HTTP_200_OK)
from .serializers import (
CropSimulationRequestSerializer,
CurrentFarmChartSerializer,
GrowthSimulationQueuedDataSerializer,
GrowthSimulationRequestSerializer,
GrowthSimulationStatusDataSerializer,
HarvestPredictionSerializer,
YieldHarvestSummarySerializer,
YieldPredictionSerializer,
)
class YieldHarvestSummaryView(APIView):
@@ -98,13 +46,7 @@ class YieldHarvestSummaryView(APIView):
@extend_schema(
tags=["Yield & Harvest Prediction"],
parameters=[
OpenApiParameter(
name="farm_uuid",
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
required=False,
description="UUID of the farm for yield and harvest prediction.",
default="11111111-1111-1111-1111-111111111111"),
farm_uuid_query_param(required=False, description="UUID of the farm for yield and harvest prediction."),
],
responses={200: status_response("YieldHarvestSummaryResponse", data=YieldHarvestSummarySerializer())},
)
@@ -151,3 +93,220 @@ class YieldHarvestSummaryView(APIView):
optimal_window_end=harvest_card.get("optimalWindowEnd") or None,
chart_data=summary.get("yield_prediction_chart", {}),
)
class CropSimulationBaseView(APIView):
@staticmethod
def _get_farm(request, farm_uuid):
if not farm_uuid:
return None, Response(
{"code": 400, "msg": "error", "data": {"farm_uuid": ["This field is required."]}},
status=status.HTTP_400_BAD_REQUEST,
)
try:
return FarmHub.objects.get(farm_uuid=farm_uuid, owner=request.user), None
except FarmHub.DoesNotExist:
return None, Response(
{"code": 404, "msg": "error", "data": {"farm_uuid": ["Farm not found."]}},
status=status.HTTP_404_NOT_FOUND,
)
@staticmethod
def _extract_result(adapter_data):
if not isinstance(adapter_data, dict):
return {}
data = adapter_data.get("data")
if isinstance(data, dict) and isinstance(data.get("result"), dict):
return data["result"]
if isinstance(data, dict):
return data
result = adapter_data.get("result")
if isinstance(result, dict):
return result
return adapter_data
@staticmethod
def _error_response(adapter_response):
response_data = (
adapter_response.data
if isinstance(adapter_response.data, dict)
else {"message": str(adapter_response.data)}
)
return Response(
{"code": adapter_response.status_code, "msg": "error", "data": response_data},
status=adapter_response.status_code,
)
class CurrentFarmChartView(CropSimulationBaseView):
ai_path = "/api/crop-simulation/current-farm-chart/"
@extend_schema(
tags=["Crop Simulation"],
request=CropSimulationRequestSerializer,
responses={200: status_response("CurrentFarmChartResponse", data=CurrentFarmChartSerializer())},
)
def post(self, request):
serializer = CropSimulationRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
payload = serializer.validated_data.copy()
farm, error_response = self._get_farm(request, payload.get("farm_uuid"))
if error_response is not None:
return error_response
ai_payload = {"farm_uuid": str(farm.farm_uuid), "plant_name": payload.get("plant_name", "")}
adapter_response = external_api_request("ai", self.ai_path, method="POST", payload=ai_payload)
if adapter_response.status_code >= 400:
return self._error_response(adapter_response)
return Response(
{"code": 200, "msg": "success", "data": self._extract_result(adapter_response.data)},
status=status.HTTP_200_OK,
)
class HarvestPredictionView(CropSimulationBaseView):
ai_path = "/api/crop-simulation/harvest-prediction/"
@extend_schema(
tags=["Crop Simulation"],
request=CropSimulationRequestSerializer,
responses={200: status_response("CropSimulationHarvestPredictionResponse", data=HarvestPredictionSerializer())},
)
def post(self, request):
serializer = CropSimulationRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
payload = serializer.validated_data.copy()
farm, error_response = self._get_farm(request, payload.get("farm_uuid"))
if error_response is not None:
return error_response
ai_payload = {"farm_uuid": str(farm.farm_uuid), "plant_name": payload.get("plant_name", "")}
adapter_response = external_api_request("ai", self.ai_path, method="POST", payload=ai_payload)
if adapter_response.status_code >= 400:
return self._error_response(adapter_response)
return Response(
{"code": 200, "msg": "success", "data": self._extract_result(adapter_response.data)},
status=status.HTTP_200_OK,
)
class YieldPredictionView(CropSimulationBaseView):
ai_path = "/api/crop-simulation/yield-prediction/"
@extend_schema(
tags=["Crop Simulation"],
request=CropSimulationRequestSerializer,
responses={200: status_response("CropSimulationYieldPredictionResponse", data=YieldPredictionSerializer())},
)
def post(self, request):
serializer = CropSimulationRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
payload = serializer.validated_data.copy()
farm, error_response = self._get_farm(request, payload.get("farm_uuid"))
if error_response is not None:
return error_response
ai_payload = {"farm_uuid": str(farm.farm_uuid), "plant_name": payload.get("plant_name", "")}
adapter_response = external_api_request("ai", self.ai_path, method="POST", payload=ai_payload)
if adapter_response.status_code >= 400:
return self._error_response(adapter_response)
return Response(
{"code": 200, "msg": "success", "data": self._extract_result(adapter_response.data)},
status=status.HTTP_200_OK,
)
class GrowthSimulationView(APIView):
@extend_schema(
tags=["Crop Simulation"],
request=GrowthSimulationRequestSerializer,
responses={202: status_response("GrowthSimulationQueuedResponse", data=GrowthSimulationQueuedDataSerializer())},
)
def post(self, request):
serializer = GrowthSimulationRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
payload = serializer.validated_data.copy()
if payload.get("farm_uuid") is not None:
payload["farm_uuid"] = str(payload["farm_uuid"])
adapter_response = external_api_request(
"ai",
"/api/crop-simulation/growth/",
method="POST",
payload=payload,
)
if adapter_response.status_code >= 400:
response_data = (
adapter_response.data
if isinstance(adapter_response.data, dict)
else {"message": str(adapter_response.data)}
)
return Response(
{"code": adapter_response.status_code, "msg": "error", "data": response_data},
status=adapter_response.status_code,
)
return Response(
{"code": 202, "msg": "تسک شبیه سازی رشد در صف قرار گرفت.", "data": CropSimulationBaseView._extract_result(adapter_response.data)},
status=status.HTTP_202_ACCEPTED,
)
class GrowthSimulationStatusView(APIView):
@extend_schema(
tags=["Crop Simulation"],
parameters=[
OpenApiParameter(name="page", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=False, description="شماره صفحه."),
OpenApiParameter(
name="page_size",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
required=False,
description="اندازه صفحه بین 1 تا 50.",
),
],
responses={200: status_response("GrowthSimulationStatusResponse", data=GrowthSimulationStatusDataSerializer())},
)
def get(self, request, task_id):
query = {}
if request.query_params.get("page"):
query["page"] = request.query_params.get("page")
if request.query_params.get("page_size"):
query["page_size"] = request.query_params.get("page_size")
adapter_response = external_api_request(
"ai",
f"/api/crop-simulation/growth/{task_id}/status/",
method="GET",
query=query or None,
)
if adapter_response.status_code >= 400:
response_data = (
adapter_response.data
if isinstance(adapter_response.data, dict)
else {"message": str(adapter_response.data)}
)
return Response(
{"code": adapter_response.status_code, "msg": "error", "data": response_data},
status=adapter_response.status_code,
)
return Response(
{"code": 200, "msg": "success", "data": CropSimulationBaseView._extract_result(adapter_response.data)},
status=status.HTTP_200_OK,
)