UPDATE
This commit is contained in:
+111
-35
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user