UPDATE
This commit is contained in:
+221
-174
@@ -1,215 +1,262 @@
|
||||
from copy import deepcopy
|
||||
from uuid import UUID
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import Http404
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.response import Response
|
||||
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 code_response
|
||||
from external_api_adapter.adapter import request as external_api_request
|
||||
from external_api_adapter.exceptions import ExternalAPIRequestError
|
||||
from farm_hub.models import FarmHub
|
||||
|
||||
from .serializers import (
|
||||
FarmUUIDRequestSerializer,
|
||||
KOptionActivateSerializer,
|
||||
LocationDataUpsertSerializer,
|
||||
)
|
||||
from .services import (
|
||||
create_zones_and_dispatch,
|
||||
ensure_latest_area_ready_for_processing,
|
||||
get_latest_cultivation_risk_payload,
|
||||
get_cultivation_risk_payload,
|
||||
get_default_area_feature,
|
||||
get_initial_zones_payload,
|
||||
get_latest_area_payload,
|
||||
get_latest_soil_quality_payload,
|
||||
get_latest_water_need_payload,
|
||||
get_products_payload,
|
||||
get_soil_quality_payload,
|
||||
get_water_need_payload,
|
||||
get_zone_details_payload,
|
||||
get_zone_page_request_params,
|
||||
AI_CLUSTER_RECOMMENDATIONS_PATH,
|
||||
AI_LOCATION_DATA_PATH,
|
||||
AI_REMOTE_SENSING_PATH,
|
||||
)
|
||||
|
||||
|
||||
AREA_QUERY_PARAMETERS = [
|
||||
OpenApiParameter(
|
||||
name="farm_uuid",
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=True,
|
||||
description="UUID مزرعه برای گرفتن يا ساخت آخرين پردازش محدوده همان مزرعه.",
|
||||
default="11111111-1111-1111-1111-111111111111"),
|
||||
OpenApiParameter(
|
||||
name="page",
|
||||
type=OpenApiTypes.INT,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=False,
|
||||
description="شماره صفحه زون ها. مقدار پيش فرض 1 است.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page_size",
|
||||
type=OpenApiTypes.INT,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=False,
|
||||
description="تعداد زون در هر صفحه. مقدار پيش فرض 10 است.",
|
||||
),
|
||||
AI_PROXY_ERROR_MESSAGE = "ارتباط با سرویس AI ناموفق بود."
|
||||
FARM_NOT_FOUND_MESSAGE = "مزرعه پیدا نشد."
|
||||
QUERY_FARM_NOT_FOUND_MESSAGE = "location پیدا نشد."
|
||||
SUCCESS_RESPONSE = code_response("LocationDataGenericSuccess", data=serializers.JSONField())
|
||||
ERROR_RESPONSE = code_response("LocationDataGenericError", data=serializers.JSONField())
|
||||
|
||||
LOCATION_DATA_QUERY_PARAMETERS = [
|
||||
OpenApiParameter("lat", OpenApiTypes.NUMBER, OpenApiParameter.QUERY, required=False),
|
||||
OpenApiParameter("lon", OpenApiTypes.NUMBER, OpenApiParameter.QUERY, required=False),
|
||||
OpenApiParameter("farm_uuid", OpenApiTypes.UUID, OpenApiParameter.QUERY, required=False),
|
||||
]
|
||||
REMOTE_SENSING_QUERY_PARAMETERS = [
|
||||
OpenApiParameter("farm_uuid", OpenApiTypes.UUID, OpenApiParameter.QUERY, required=True),
|
||||
OpenApiParameter("page", OpenApiTypes.INT, OpenApiParameter.QUERY, required=False),
|
||||
OpenApiParameter("page_size", OpenApiTypes.INT, OpenApiParameter.QUERY, required=False),
|
||||
OpenApiParameter("start_date", OpenApiTypes.DATE, OpenApiParameter.QUERY, required=False),
|
||||
OpenApiParameter("end_date", OpenApiTypes.DATE, OpenApiParameter.QUERY, required=False),
|
||||
]
|
||||
CLUSTER_BLOCK_LIVE_QUERY_PARAMETERS = [
|
||||
OpenApiParameter("start_date", OpenApiTypes.DATE, OpenApiParameter.QUERY, required=False),
|
||||
OpenApiParameter("end_date", OpenApiTypes.DATE, OpenApiParameter.QUERY, required=False),
|
||||
OpenApiParameter("farm_uuid", OpenApiTypes.UUID, OpenApiParameter.QUERY, required=False),
|
||||
]
|
||||
OPTIONAL_FARM_UUID_QUERY_PARAMETER = [
|
||||
OpenApiParameter("farm_uuid", OpenApiTypes.UUID, OpenApiParameter.QUERY, required=False),
|
||||
]
|
||||
|
||||
|
||||
class BaseAreaDataView(APIView):
|
||||
payload_getter = None
|
||||
class AILocationDataProxyView(APIView):
|
||||
ai_path = AI_LOCATION_DATA_PATH
|
||||
farm_uuid_locations = ()
|
||||
farm_not_found_message = FARM_NOT_FOUND_MESSAGE
|
||||
|
||||
def get(self, request):
|
||||
farm_uuid = request.query_params.get("farm_uuid")
|
||||
def _build_path(self, **kwargs):
|
||||
return self.ai_path.format(**kwargs)
|
||||
|
||||
def _get_payload(self, request):
|
||||
if not request.data:
|
||||
return None
|
||||
if isinstance(request.data, dict):
|
||||
return deepcopy(request.data)
|
||||
return request.data
|
||||
|
||||
def _get_query(self, request):
|
||||
if not request.query_params:
|
||||
return None
|
||||
query = {}
|
||||
for key, values in request.query_params.lists():
|
||||
query[key] = values if len(values) > 1 else values[0]
|
||||
return query
|
||||
|
||||
def _parse_uuid(self, value):
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
page, page_size = get_zone_page_request_params(request.query_params)
|
||||
crop_area = ensure_latest_area_ready_for_processing(farm_uuid=farm_uuid, owner=request.user)
|
||||
except ValueError as exc:
|
||||
return Response({"status": "error", "message": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except ImproperlyConfigured as exc:
|
||||
return Response({"status": "error", "message": str(exc)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return UUID(str(value))
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
def _extract_farm_uuid(self, request, payload, query):
|
||||
for location in self.farm_uuid_locations:
|
||||
if location == "body" and isinstance(payload, dict) and payload.get("farm_uuid"):
|
||||
parsed = self._parse_uuid(payload.get("farm_uuid"))
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
if location == "query" and isinstance(query, dict) and query.get("farm_uuid"):
|
||||
parsed = self._parse_uuid(query.get("farm_uuid"))
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
return None
|
||||
|
||||
def _ensure_farm_access(self, request, farm_uuid):
|
||||
if farm_uuid is None:
|
||||
return None
|
||||
if FarmHub.objects.filter(farm_uuid=farm_uuid, owner=request.user).exists():
|
||||
return None
|
||||
return Response(
|
||||
{"status": "success", "data": self.payload_getter(crop_area, page=page, page_size=page_size)},
|
||||
status=status.HTTP_200_OK,
|
||||
{"code": 404, "msg": self.farm_not_found_message, "data": None},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
class AreaView(BaseAreaDataView):
|
||||
payload_getter = staticmethod(get_latest_area_payload)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
parameters=AREA_QUERY_PARAMETERS,
|
||||
responses={
|
||||
200: status_response("CropZoningAreaResponse", data=serializers.JSONField()),
|
||||
400: status_response("CropZoningAreaValidationError", data=serializers.JSONField()),
|
||||
500: status_response("CropZoningAreaServerError", data=serializers.JSONField()),
|
||||
},
|
||||
)
|
||||
def get(self, request):
|
||||
return super().get(request)
|
||||
|
||||
|
||||
class WaterNeedView(BaseAreaDataView):
|
||||
payload_getter = staticmethod(get_latest_water_need_payload)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
parameters=AREA_QUERY_PARAMETERS,
|
||||
responses={
|
||||
200: status_response("CropZoningWaterNeedResponse", data=serializers.JSONField()),
|
||||
400: status_response("CropZoningWaterNeedValidationError", data=serializers.JSONField()),
|
||||
500: status_response("CropZoningWaterNeedServerError", data=serializers.JSONField()),
|
||||
},
|
||||
)
|
||||
def get(self, request):
|
||||
return super().get(request)
|
||||
|
||||
|
||||
class SoilQualityView(BaseAreaDataView):
|
||||
payload_getter = staticmethod(get_latest_soil_quality_payload)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
parameters=AREA_QUERY_PARAMETERS,
|
||||
responses={
|
||||
200: status_response("CropZoningSoilQualityResponse", data=serializers.JSONField()),
|
||||
400: status_response("CropZoningSoilQualityValidationError", data=serializers.JSONField()),
|
||||
500: status_response("CropZoningSoilQualityServerError", data=serializers.JSONField()),
|
||||
},
|
||||
)
|
||||
def get(self, request):
|
||||
return super().get(request)
|
||||
|
||||
|
||||
class CultivationRiskView(BaseAreaDataView):
|
||||
payload_getter = staticmethod(get_latest_cultivation_risk_payload)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
parameters=AREA_QUERY_PARAMETERS,
|
||||
responses={
|
||||
200: status_response("CropZoningCultivationRiskResponse", data=serializers.JSONField()),
|
||||
400: status_response("CropZoningCultivationRiskValidationError", data=serializers.JSONField()),
|
||||
500: status_response("CropZoningCultivationRiskServerError", data=serializers.JSONField()),
|
||||
},
|
||||
)
|
||||
def get(self, request):
|
||||
return super().get(request)
|
||||
|
||||
|
||||
class ProductsView(APIView):
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
responses={200: status_response("CropZoningProductsResponse", data=serializers.JSONField())},
|
||||
)
|
||||
def get(self, request):
|
||||
return Response({"status": "success", "data": get_products_payload()}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ZonesInitialView(APIView):
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
request=OpenApiTypes.OBJECT,
|
||||
responses={200: status_response("CropZoningZonesInitialResponse", data=serializers.JSONField())},
|
||||
)
|
||||
def post(self, request):
|
||||
area_feature = (
|
||||
request.data.get("area")
|
||||
or request.data.get("area_geojson")
|
||||
or request.data.get("boundary")
|
||||
or get_default_area_feature()
|
||||
def _build_proxy_error(self, exc):
|
||||
return Response(
|
||||
{"code": 502, "msg": AI_PROXY_ERROR_MESSAGE, "data": {"detail": str(exc)}},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
cell_side_km = request.data.get("cell_side_km")
|
||||
|
||||
def _proxy(self, request, *, method, **path_kwargs):
|
||||
payload = self._get_payload(request)
|
||||
query = self._get_query(request)
|
||||
farm_uuid = self._extract_farm_uuid(request, payload, query)
|
||||
farm_error = self._ensure_farm_access(request, farm_uuid)
|
||||
if farm_error is not None:
|
||||
return farm_error
|
||||
|
||||
try:
|
||||
crop_area, _zones = create_zones_and_dispatch(area_feature, cell_side_km=cell_side_km)
|
||||
except ValueError as exc:
|
||||
return Response({"status": "error", "message": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except ImproperlyConfigured as exc:
|
||||
return Response({"status": "error", "message": str(exc)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
adapter_response = external_api_request(
|
||||
"ai",
|
||||
self._build_path(**path_kwargs),
|
||||
method=method,
|
||||
payload=payload,
|
||||
query=query,
|
||||
)
|
||||
except (ExternalAPIRequestError, ImproperlyConfigured) as exc:
|
||||
return self._build_proxy_error(exc)
|
||||
|
||||
return Response({"status": "success", "data": get_initial_zones_payload(crop_area)}, status=status.HTTP_200_OK)
|
||||
response_payload = adapter_response.data
|
||||
if not isinstance(response_payload, dict):
|
||||
response_payload = {
|
||||
"code": adapter_response.status_code,
|
||||
"msg": "success" if adapter_response.status_code < 400 else "error",
|
||||
"data": response_payload,
|
||||
}
|
||||
return Response(response_payload, status=adapter_response.status_code)
|
||||
|
||||
|
||||
class ZonesWaterNeedView(APIView):
|
||||
class LocationDataView(AILocationDataProxyView):
|
||||
farm_uuid_locations = ("query", "body")
|
||||
farm_not_found_message = QUERY_FARM_NOT_FOUND_MESSAGE
|
||||
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
request=OpenApiTypes.OBJECT,
|
||||
responses={200: status_response("CropZoningZonesWaterNeedResponse", data=serializers.JSONField())},
|
||||
tags=["Location Data"],
|
||||
parameters=LOCATION_DATA_QUERY_PARAMETERS,
|
||||
responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE},
|
||||
)
|
||||
def get(self, request):
|
||||
return self._proxy(request, method="GET")
|
||||
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
request=LocationDataUpsertSerializer,
|
||||
responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE},
|
||||
)
|
||||
def post(self, request):
|
||||
zone_ids = request.data.get("zoneIds")
|
||||
return Response({"status": "success", "data": get_water_need_payload(zone_ids)}, status=status.HTTP_200_OK)
|
||||
return self._proxy(request, method="POST")
|
||||
|
||||
|
||||
class ZonesSoilQualityView(APIView):
|
||||
class LocationDataNdviHealthView(AILocationDataProxyView):
|
||||
ai_path = f"{AI_LOCATION_DATA_PATH}ndvi-health/"
|
||||
farm_uuid_locations = ("body",)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
request=OpenApiTypes.OBJECT,
|
||||
responses={200: status_response("CropZoningZonesSoilQualityResponse", data=serializers.JSONField())},
|
||||
tags=["Location Data"],
|
||||
request=FarmUUIDRequestSerializer,
|
||||
responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE},
|
||||
)
|
||||
def post(self, request):
|
||||
zone_ids = request.data.get("zoneIds")
|
||||
return Response({"status": "success", "data": get_soil_quality_payload(zone_ids)}, status=status.HTTP_200_OK)
|
||||
return self._proxy(request, method="POST")
|
||||
|
||||
|
||||
class ZonesCultivationRiskView(APIView):
|
||||
class LocationDataRemoteSensingView(AILocationDataProxyView):
|
||||
ai_path = AI_REMOTE_SENSING_PATH
|
||||
farm_uuid_locations = ("query", "body")
|
||||
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
request=OpenApiTypes.OBJECT,
|
||||
responses={200: status_response("CropZoningZonesCultivationRiskResponse", data=serializers.JSONField())},
|
||||
tags=["Location Data"],
|
||||
parameters=REMOTE_SENSING_QUERY_PARAMETERS,
|
||||
responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE},
|
||||
)
|
||||
def get(self, request):
|
||||
return self._proxy(request, method="GET")
|
||||
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
request=FarmUUIDRequestSerializer,
|
||||
responses={202: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE},
|
||||
)
|
||||
def post(self, request):
|
||||
zone_ids = request.data.get("zoneIds")
|
||||
return Response({"status": "success", "data": get_cultivation_risk_payload(zone_ids)}, status=status.HTTP_200_OK)
|
||||
return self._proxy(request, method="POST")
|
||||
|
||||
|
||||
class ZoneDetailsView(APIView):
|
||||
class ClusterBlockLiveView(AILocationDataProxyView):
|
||||
ai_path = f"{AI_REMOTE_SENSING_PATH}cluster-blocks/{{cluster_uuid}}/live/"
|
||||
farm_uuid_locations = ("query",)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Crop Zoning"],
|
||||
responses={200: status_response("CropZoningZoneDetailsResponse", data=serializers.JSONField())},
|
||||
tags=["Location Data"],
|
||||
parameters=CLUSTER_BLOCK_LIVE_QUERY_PARAMETERS,
|
||||
responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE, 502: ERROR_RESPONSE},
|
||||
)
|
||||
def get(self, request, zone_id):
|
||||
try:
|
||||
data = get_zone_details_payload(zone_id)
|
||||
except Exception as exc:
|
||||
if exc.__class__.__name__ == "DoesNotExist":
|
||||
raise Http404("Zone not found")
|
||||
raise
|
||||
return Response({"status": "success", "data": data}, status=status.HTTP_200_OK)
|
||||
def get(self, request, cluster_uuid):
|
||||
return self._proxy(request, method="GET", cluster_uuid=cluster_uuid)
|
||||
|
||||
|
||||
class ClusterRecommendationsView(AILocationDataProxyView):
|
||||
ai_path = AI_CLUSTER_RECOMMENDATIONS_PATH
|
||||
farm_uuid_locations = ("query",)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
parameters=REMOTE_SENSING_QUERY_PARAMETERS[:1],
|
||||
responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE},
|
||||
)
|
||||
def get(self, request):
|
||||
return self._proxy(request, method="GET")
|
||||
|
||||
|
||||
class KOptionsView(AILocationDataProxyView):
|
||||
ai_path = f"{AI_REMOTE_SENSING_PATH}results/{{result_id}}/k-options/"
|
||||
farm_uuid_locations = ("query",)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
parameters=OPTIONAL_FARM_UUID_QUERY_PARAMETER,
|
||||
responses={200: SUCCESS_RESPONSE, 404: ERROR_RESPONSE},
|
||||
)
|
||||
def get(self, request, result_id):
|
||||
return self._proxy(request, method="GET", result_id=result_id)
|
||||
|
||||
|
||||
class KOptionsActivateView(AILocationDataProxyView):
|
||||
ai_path = f"{AI_REMOTE_SENSING_PATH}results/{{result_id}}/k-options/activate/"
|
||||
farm_uuid_locations = ("query", "body")
|
||||
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
parameters=OPTIONAL_FARM_UUID_QUERY_PARAMETER,
|
||||
request=KOptionActivateSerializer,
|
||||
responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE},
|
||||
)
|
||||
def post(self, request, result_id):
|
||||
return self._proxy(request, method="POST", result_id=result_id)
|
||||
|
||||
|
||||
class RunStatusView(AILocationDataProxyView):
|
||||
ai_path = f"{AI_REMOTE_SENSING_PATH}runs/{{run_id}}/status/"
|
||||
farm_uuid_locations = ("query",)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
parameters=OPTIONAL_FARM_UUID_QUERY_PARAMETER,
|
||||
responses={200: SUCCESS_RESPONSE, 404: ERROR_RESPONSE},
|
||||
)
|
||||
def get(self, request, run_id):
|
||||
return self._proxy(request, method="GET", run_id=run_id)
|
||||
|
||||
Reference in New Issue
Block a user