from rest_framework import status from drf_spectacular.utils import ( OpenApiExample, OpenApiResponse, extend_schema, inline_serializer, ) from rest_framework import serializers as drf_serializers from rest_framework.response import Response from rest_framework.views import APIView from config.openapi import ( build_envelope_serializer, build_response, build_task_status_data_serializer, ) from .models import SoilLocation from .serializers import ( SoilDataRequestSerializer, SoilDepthDataSerializer, SoilDataTaskResponseSerializer, SoilLocationResponseSerializer, ) from .tasks import fetch_soil_data_task SoilLocationPayloadSerializer = inline_serializer( name="SoilLocationPayloadSerializer", fields={ "source": drf_serializers.CharField(), "id": drf_serializers.IntegerField(), "lon": drf_serializers.DecimalField(max_digits=9, decimal_places=6), "lat": drf_serializers.DecimalField(max_digits=9, decimal_places=6), "depths": SoilDepthDataSerializer(many=True), }, ) SoilDataResponseSerializer = build_envelope_serializer( "SoilDataResponseSerializer", SoilLocationPayloadSerializer, ) SoilTaskQueuedResponseSerializer = build_envelope_serializer( "SoilTaskQueuedResponseSerializer", SoilDataTaskResponseSerializer, ) SoilErrorResponseSerializer = build_envelope_serializer( "SoilErrorResponseSerializer", data_required=False, allow_null=True, ) SoilTaskStatusResponseSerializer = build_envelope_serializer( "SoilTaskStatusResponseSerializer", build_task_status_data_serializer("SoilTaskStatusDataSerializer"), ) class SoilDataView(APIView): """ API خاک: مختصات جغرافیایی را می‌گیرد. اگر داده در DB موجود باشد، برگردانده می‌شود؛ در غیر این صورت تسک Celery صف می‌شود و task_id برمی‌گردد. """ def _get_request_data(self, request): return request.data if request.method == "POST" else request.query_params @extend_schema( tags=["Soil Data"], summary="دریافت داده خاک (GET)", description="با ارسال lat و lon، داده خاک از DB یا از طریق تسک Celery برگردانده می‌شود.", parameters=[ { "name": "lat", "in": "query", "required": True, "schema": {"type": "number"}, "description": "عرض جغرافیایی", }, { "name": "lon", "in": "query", "required": True, "schema": {"type": "number"}, "description": "طول جغرافیایی", }, ], responses={ 200: build_response( SoilDataResponseSerializer, "داده خاک از دیتابیس بازگردانده شد.", ), 202: build_response( SoilTaskQueuedResponseSerializer, "تسک واکشی داده خاک در صف قرار گرفت.", ), 400: build_response( SoilErrorResponseSerializer, "پارامترهای ورودی نامعتبر هستند.", ), }, ) def get(self, request): return self._process(request) @extend_schema( tags=["Soil Data"], summary="دریافت داده خاک (POST)", description="با ارسال lat و lon در بدنه، داده خاک از DB یا از طریق تسک Celery برگردانده می‌شود.", request=SoilDataRequestSerializer, responses={ 200: build_response( SoilDataResponseSerializer, "داده خاک از دیتابیس بازگردانده شد.", ), 202: build_response( SoilTaskQueuedResponseSerializer, "تسک واکشی داده خاک در صف قرار گرفت.", ), 400: build_response( SoilErrorResponseSerializer, "پارامترهای ورودی نامعتبر هستند.", ), }, examples=[ OpenApiExample( "نمونه درخواست", value={"lat": 35.6892, "lon": 51.3890}, request_only=True, ), ], ) def post(self, request): return self._process(request) def _process(self, request): data = self._get_request_data(request) serializer = SoilDataRequestSerializer(data=data) if not serializer.is_valid(): return Response( {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) lat = serializer.validated_data["lat"] lon = serializer.validated_data["lon"] lat_rounded = round(lat, 6) lon_rounded = round(lon, 6) location = ( SoilLocation.objects.filter( latitude=lat_rounded, longitude=lon_rounded, ) .prefetch_related("depths") .first() ) if location and location.is_complete: data_serializer = SoilLocationResponseSerializer(location) return Response( { "code": 200, "msg": "success", "data": { "source": "database", **data_serializer.data, }, }, status=status.HTTP_200_OK, ) result = fetch_soil_data_task.delay(float(lat_rounded), float(lon_rounded)) task_data = SoilDataTaskResponseSerializer( { "task_id": result.id, "longitude": float(lon_rounded), "latitude": float(lat_rounded), "status_url": f"/api/soil-data/tasks/{result.id}/status/", } ).data return Response( { "code": 202, "msg": "تسک در صف. وضعیت را با task_id بررسی کنید.", "data": task_data, }, status=status.HTTP_202_ACCEPTED, ) class SoilDataTaskStatusView(APIView): """وضعیت تسک واکشی خاک. در صورت SUCCESS لیست اطلاعات هر سه عمق برگردانده می‌شود.""" @extend_schema( tags=["Soil Data"], summary="وضعیت تسک داده خاک", description="وضعیت تسک Celery واکشی داده خاک را برمی‌گرداند.", responses={ 200: build_response( SoilTaskStatusResponseSerializer, "وضعیت فعلی تسک واکشی داده خاک.", ), }, ) def get(self, request, task_id): from celery.result import AsyncResult result = AsyncResult(task_id) state = result.state data = {"task_id": task_id, "status": state} if state == "PENDING": data["message"] = "تسک در صف یا یافت نشد." elif state == "PROGRESS": data["progress"] = result.info elif state == "SUCCESS": task_result = result.result if isinstance(task_result, dict) and task_result.get("status") == "completed": location_id = task_result.get("location_id") location = ( SoilLocation.objects.filter(pk=location_id) .prefetch_related("depths") .first() ) if location and location.is_complete: data["result"] = SoilLocationResponseSerializer(location).data else: data["result"] = task_result else: data["result"] = task_result elif state == "FAILURE": data["error"] = str(result.result) return Response( {"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK, )