diff --git a/config/openapi.py b/config/openapi.py new file mode 100644 index 0000000..5d913f3 --- /dev/null +++ b/config/openapi.py @@ -0,0 +1,103 @@ +import copy + +from drf_spectacular.utils import OpenApiResponse +from rest_framework import serializers + + +def _build_schema_field(schema, *, many=False, required=True, allow_null=False): + if schema is None: + return serializers.JSONField(required=required, allow_null=allow_null) + + if isinstance(schema, serializers.Field): + field = copy.deepcopy(schema) + field.required = required + if hasattr(field, "allow_null"): + field.allow_null = allow_null + return field + + if isinstance(schema, serializers.BaseSerializer): + serializer = copy.deepcopy(schema) + serializer.required = required + serializer.allow_null = allow_null + return serializer + + if isinstance(schema, type) and issubclass(schema, serializers.BaseSerializer): + return schema(many=many, required=required, allow_null=allow_null) + + raise TypeError(f"Unsupported schema type: {type(schema)!r}") + + +def build_message_response_serializer(name): + return type( + name, + (serializers.Serializer,), + { + "__module__": __name__, + "code": serializers.IntegerField(), + "msg": serializers.CharField(), + }, + ) + + +def build_envelope_serializer( + name, + data_schema=None, + *, + many=False, + data_required=True, + allow_null=False, +): + return type( + name, + (serializers.Serializer,), + { + "__module__": __name__, + "code": serializers.IntegerField(), + "msg": serializers.CharField(), + "data": _build_schema_field( + data_schema, + many=many, + required=data_required, + allow_null=allow_null, + ), + }, + ) + + +def build_task_queue_data_serializer(name, extra_fields=None): + fields = { + "__module__": __name__, + "task_id": serializers.CharField(), + "status_url": serializers.CharField(), + } + if extra_fields: + fields.update(extra_fields) + return type(name, (serializers.Serializer,), fields) + + +def build_task_status_data_serializer(name, result_schema=None): + result_field = ( + _build_schema_field(result_schema, required=False, allow_null=True) + if result_schema is not None + else serializers.JSONField(required=False) + ) + return type( + name, + (serializers.Serializer,), + { + "__module__": __name__, + "task_id": serializers.CharField(), + "status": serializers.CharField(), + "message": serializers.CharField(required=False), + "progress": serializers.DictField( + child=serializers.JSONField(), + required=False, + ), + "result": result_field, + "error": serializers.CharField(required=False), + }, + ) + + +def build_response(serializer, description): + return OpenApiResponse(response=serializer, description=description) diff --git a/config/settings.py b/config/settings.py index 9f0bb76..595a391 100644 --- a/config/settings.py +++ b/config/settings.py @@ -122,10 +122,15 @@ SPECTACULAR_SETTINGS = { "TAGS": [ {"name": "Dashboard Data", "description": "تجمیع داده‌های داشبورد مزرعه"}, {"name": "RAG Chat", "description": "چت هوشمند RAG"}, + {"name": "RAG Recommendations", "description": "توصیه‌های آبیاری و کودهی مبتنی بر RAG"}, {"name": "Tasks", "description": "مدیریت تسک‌های Celery"}, {"name": "Soil Data", "description": "داده‌های خاک (SoilGrids)"}, {"name": "Sensor Data", "description": "داده‌های سنسور"}, {"name": "Sensor Parameters", "description": "پارامترهای سنسور"}, + {"name": "Plant", "description": "مدیریت گیاهان و دریافت اطلاعات گیاه"}, + {"name": "Irrigation", "description": "مدیریت روش‌های آبیاری"}, + {"name": "Irrigation Recommendation", "description": "درخواست و پیگیری توصیه آبیاری"}, + {"name": "Fertilization Recommendation", "description": "درخواست و پیگیری توصیه کودهی"}, ], } diff --git a/dashboard_data/views.py b/dashboard_data/views.py index 0730272..e5503b7 100644 --- a/dashboard_data/views.py +++ b/dashboard_data/views.py @@ -7,9 +7,34 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView +from config.openapi import ( + build_envelope_serializer, + build_response, + build_task_queue_data_serializer, + build_task_status_data_serializer, +) + from .tasks import generate_dashboard_data_task +DashboardDataGenerateDataSerializer = build_task_queue_data_serializer( + "DashboardDataGenerateDataSerializer" +) +DashboardDataGenerateResponseSerializer = build_envelope_serializer( + "DashboardDataGenerateResponseSerializer", + DashboardDataGenerateDataSerializer, +) +DashboardDataErrorResponseSerializer = build_envelope_serializer( + "DashboardDataErrorResponseSerializer", + data_required=False, + allow_null=True, +) +DashboardDataStatusResponseSerializer = build_envelope_serializer( + "DashboardDataStatusResponseSerializer", + build_task_status_data_serializer("DashboardDataStatusDataSerializer"), +) + + class DashboardDataGenerateView(APIView): @extend_schema( tags=["Dashboard Data"], @@ -22,21 +47,14 @@ class DashboardDataGenerateView(APIView): }, ), responses={ - 202: inline_serializer( - name="DashboardDataGenerateResponse", - fields={ - "code": drf_serializers.IntegerField(), - "msg": drf_serializers.CharField(), - "data": inline_serializer( - name="DashboardDataGenerateResponseData", - fields={ - "task_id": drf_serializers.CharField(), - "status_url": drf_serializers.CharField(), - }, - ), - }, + 202: build_response( + DashboardDataGenerateResponseSerializer, + "تسک ساخت داده داشبورد در صف قرار گرفت.", + ), + 400: build_response( + DashboardDataErrorResponseSerializer, + "پارامتر ورودی نامعتبر است.", ), - 400: OpenApiResponse(description="Invalid input"), }, ) def post(self, request): @@ -72,6 +90,12 @@ class DashboardDataStatusView(APIView): @extend_schema( tags=["Dashboard Data"], summary="Dashboard task status", + responses={ + 200: build_response( + DashboardDataStatusResponseSerializer, + "وضعیت فعلی تسک داده داشبورد.", + ), + }, ) def get(self, request, task_id): result = AsyncResult(task_id) diff --git a/fertilization/views.py b/fertilization/views.py index 77eb57f..4654df1 100644 --- a/fertilization/views.py +++ b/fertilization/views.py @@ -7,9 +7,31 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView +from config.openapi import ( + build_envelope_serializer, + build_response, + build_task_queue_data_serializer, + build_task_status_data_serializer, +) + from .serializers import FertilizationRecommendRequestSerializer +FertilizationQueueResponseSerializer = build_envelope_serializer( + "FertilizationQueueResponseSerializer", + build_task_queue_data_serializer("FertilizationQueueDataSerializer"), +) +FertilizationValidationErrorSerializer = build_envelope_serializer( + "FertilizationValidationErrorSerializer", + data_required=False, + allow_null=True, +) +FertilizationStatusResponseSerializer = build_envelope_serializer( + "FertilizationStatusResponseSerializer", + build_task_status_data_serializer("FertilizationStatusDataSerializer"), +) + + class FertilizationRecommendView(APIView): """ توصیه کودهی با Celery. @@ -29,8 +51,14 @@ class FertilizationRecommendView(APIView): ), request=FertilizationRecommendRequestSerializer, responses={ - 202: OpenApiResponse(description="تسک در صف قرار گرفت"), - 400: OpenApiResponse(description="پارامتر ورودی نامعتبر"), + 202: build_response( + FertilizationQueueResponseSerializer, + "تسک توصیه کودهی در صف قرار گرفت.", + ), + 400: build_response( + FertilizationValidationErrorSerializer, + "پارامتر ورودی نامعتبر است.", + ), }, examples=[ OpenApiExample( @@ -87,7 +115,10 @@ class FertilizationRecommendStatusView(APIView): summary="وضعیت تسک توصیه کودهی", description="وضعیت تسک Celery توصیه کودهی را برمی‌گرداند.", responses={ - 200: OpenApiResponse(description="وضعیت تسک"), + 200: build_response( + FertilizationStatusResponseSerializer, + "وضعیت فعلی تسک توصیه کودهی.", + ), }, ) def get(self, request, task_id): diff --git a/irrigation/views.py b/irrigation/views.py index 3aa31f4..126ef27 100644 --- a/irrigation/views.py +++ b/irrigation/views.py @@ -1,12 +1,15 @@ -from drf_spectacular.utils import ( - OpenApiExample, - OpenApiResponse, - extend_schema, -) +from drf_spectacular.utils import OpenApiExample, extend_schema from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView +from config.openapi import ( + build_envelope_serializer, + build_response, + build_task_queue_data_serializer, + build_task_status_data_serializer, +) + from .models import IrrigationMethod from .serializers import ( IrrigationMethodSerializer, @@ -14,6 +17,30 @@ from .serializers import ( ) +IrrigationMethodListResponseSerializer = build_envelope_serializer( + "IrrigationMethodListResponseSerializer", + IrrigationMethodSerializer, + many=True, +) +IrrigationMethodDetailResponseSerializer = build_envelope_serializer( + "IrrigationMethodDetailResponseSerializer", + IrrigationMethodSerializer, +) +IrrigationValidationErrorSerializer = build_envelope_serializer( + "IrrigationValidationErrorSerializer", + data_required=False, + allow_null=True, +) +IrrigationQueueResponseSerializer = build_envelope_serializer( + "IrrigationQueueResponseSerializer", + build_task_queue_data_serializer("IrrigationQueueDataSerializer"), +) +IrrigationStatusResponseSerializer = build_envelope_serializer( + "IrrigationStatusResponseSerializer", + build_task_status_data_serializer("IrrigationStatusDataSerializer"), +) + + class IrrigationMethodListCreateView(APIView): """لیست تمام روش‌های آبیاری و ایجاد روش جدید.""" @@ -21,7 +48,12 @@ class IrrigationMethodListCreateView(APIView): tags=["Irrigation"], summary="لیست روش‌های آبیاری", description="لیست تمام روش‌های آبیاری ذخیره‌شده را برمی‌گرداند.", - responses={200: IrrigationMethodSerializer(many=True)}, + responses={ + 200: build_response( + IrrigationMethodListResponseSerializer, + "لیست روش‌های آبیاری ذخیره‌شده.", + ), + }, ) def get(self, request): methods = IrrigationMethod.objects.all() @@ -31,6 +63,52 @@ class IrrigationMethodListCreateView(APIView): status=status.HTTP_200_OK, ) + @extend_schema( + tags=["Irrigation"], + summary="ایجاد روش آبیاری جدید", + description="یک روش آبیاری جدید ایجاد می‌کند.", + request=IrrigationMethodSerializer, + responses={ + 201: build_response( + IrrigationMethodDetailResponseSerializer, + "روش آبیاری جدید با موفقیت ایجاد شد.", + ), + 400: build_response( + IrrigationValidationErrorSerializer, + "داده ورودی نامعتبر است.", + ), + }, + examples=[ + OpenApiExample( + "نمونه درخواست", + value={ + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و فشار مناسب", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "تمام انواع خاک", + "climate_suitability": "گرم و خشک", + }, + request_only=True, + ), + ], + ) + def post(self, request): + serializer = IrrigationMethodSerializer(data=request.data) + if not serializer.is_valid(): + return Response( + {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + serializer.save() + return Response( + {"code": 201, "msg": "success", "data": serializer.data}, + status=status.HTTP_201_CREATED, + ) + class IrrigationRecommendView(APIView): """ @@ -50,8 +128,14 @@ class IrrigationRecommendView(APIView): ), request=IrrigationRecommendRequestSerializer, responses={ - 202: OpenApiResponse(description="تسک در صف قرار گرفت"), - 400: OpenApiResponse(description="پارامتر ورودی نامعتبر"), + 202: build_response( + IrrigationQueueResponseSerializer, + "تسک توصیه آبیاری در صف قرار گرفت.", + ), + 400: build_response( + IrrigationValidationErrorSerializer, + "پارامتر ورودی نامعتبر است.", + ), }, examples=[ OpenApiExample( @@ -111,7 +195,10 @@ class IrrigationRecommendStatusView(APIView): summary="وضعیت تسک توصیه آبیاری", description="وضعیت تسک Celery توصیه آبیاری را برمی‌گرداند.", responses={ - 200: OpenApiResponse(description="وضعیت تسک"), + 200: build_response( + IrrigationStatusResponseSerializer, + "وضعیت فعلی تسک توصیه آبیاری.", + ), }, ) def get(self, request, task_id): @@ -132,46 +219,6 @@ class IrrigationRecommendStatusView(APIView): status=status.HTTP_200_OK, ) - @extend_schema( - tags=["Irrigation"], - summary="ایجاد روش آبیاری جدید", - description="یک روش آبیاری جدید ایجاد می‌کند.", - request=IrrigationMethodSerializer, - responses={ - 201: IrrigationMethodSerializer, - 400: OpenApiResponse(description="داده نامعتبر"), - }, - examples=[ - OpenApiExample( - "نمونه درخواست", - value={ - "name": "آبیاری قطره‌ای", - "category": "موضعی", - "description": "آبیاری با دبی کم و فشار مناسب", - "water_efficiency_percent": 90.0, - "water_pressure_required": "۱-۲ اتمسفر", - "flow_rate": "۲-۸ لیتر در ساعت", - "coverage_area": "بسته به طراحی سیستم", - "soil_type": "تمام انواع خاک", - "climate_suitability": "گرم و خشک", - }, - request_only=True, - ), - ], - ) - def post(self, request): - serializer = IrrigationMethodSerializer(data=request.data) - if not serializer.is_valid(): - return Response( - {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - serializer.save() - return Response( - {"code": 201, "msg": "success", "data": serializer.data}, - status=status.HTTP_201_CREATED, - ) - class IrrigationMethodDetailView(APIView): """دریافت، ویرایش و حذف یک روش آبیاری.""" @@ -184,8 +231,14 @@ class IrrigationMethodDetailView(APIView): summary="جزئیات روش آبیاری", description="مشخصات یک روش آبیاری را بر اساس شناسه برمی‌گرداند.", responses={ - 200: IrrigationMethodSerializer, - 404: OpenApiResponse(description="روش آبیاری یافت نشد"), + 200: build_response( + IrrigationMethodDetailResponseSerializer, + "جزئیات روش آبیاری.", + ), + 404: build_response( + IrrigationValidationErrorSerializer, + "روش آبیاری یافت نشد.", + ), }, ) def get(self, request, pk): @@ -207,9 +260,18 @@ class IrrigationMethodDetailView(APIView): description="تمام فیلدهای یک روش آبیاری را آپدیت می‌کند.", request=IrrigationMethodSerializer, responses={ - 200: IrrigationMethodSerializer, - 400: OpenApiResponse(description="داده نامعتبر"), - 404: OpenApiResponse(description="روش آبیاری یافت نشد"), + 200: build_response( + IrrigationMethodDetailResponseSerializer, + "روش آبیاری با موفقیت به‌روزرسانی شد.", + ), + 400: build_response( + IrrigationValidationErrorSerializer, + "داده ورودی نامعتبر است.", + ), + 404: build_response( + IrrigationValidationErrorSerializer, + "روش آبیاری یافت نشد.", + ), }, ) def put(self, request, pk): @@ -237,9 +299,18 @@ class IrrigationMethodDetailView(APIView): description="فقط فیلدهای ارسال‌شده آپدیت می‌شوند.", request=IrrigationMethodSerializer, responses={ - 200: IrrigationMethodSerializer, - 400: OpenApiResponse(description="داده نامعتبر"), - 404: OpenApiResponse(description="روش آبیاری یافت نشد"), + 200: build_response( + IrrigationMethodDetailResponseSerializer, + "روش آبیاری با موفقیت به‌روزرسانی شد.", + ), + 400: build_response( + IrrigationValidationErrorSerializer, + "داده ورودی نامعتبر است.", + ), + 404: build_response( + IrrigationValidationErrorSerializer, + "روش آبیاری یافت نشد.", + ), }, ) def patch(self, request, pk): @@ -266,8 +337,14 @@ class IrrigationMethodDetailView(APIView): summary="حذف روش آبیاری", description="یک روش آبیاری را حذف می‌کند.", responses={ - 200: OpenApiResponse(description="حذف موفق"), - 404: OpenApiResponse(description="روش آبیاری یافت نشد"), + 200: build_response( + IrrigationValidationErrorSerializer, + "روش آبیاری با موفقیت حذف شد.", + ), + 404: build_response( + IrrigationValidationErrorSerializer, + "روش آبیاری یافت نشد.", + ), }, ) def delete(self, request, pk): diff --git a/json/mock_data/dashboard-data/generate/post_202.json b/json/mock_data/dashboard-data/generate/post_202.json new file mode 100644 index 0000000..34b0f71 --- /dev/null +++ b/json/mock_data/dashboard-data/generate/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "dashboard task queued", + "data": { + "task_id": "dashboard-task-123", + "status_url": "/api/dashboard-data/dashboard-task-123/status/" + } +} diff --git a/json/mock_data/dashboard-data/generate/post_400.json b/json/mock_data/dashboard-data/generate/post_400.json new file mode 100644 index 0000000..5df03b8 --- /dev/null +++ b/json/mock_data/dashboard-data/generate/post_400.json @@ -0,0 +1,5 @@ +{ + "code": 400, + "msg": "پارامتر sensor_id الزامی است.", + "data": null +} diff --git a/json/mock_data/dashboard-data/status/get_200_failure.json b/json/mock_data/dashboard-data/status/get_200_failure.json new file mode 100644 index 0000000..bc4f2eb --- /dev/null +++ b/json/mock_data/dashboard-data/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "dashboard-task-123", + "status": "FAILURE", + "error": "خطا در ساخت کارت‌های داشبورد." + } +} diff --git a/json/mock_data/dashboard-data/status/get_200_pending.json b/json/mock_data/dashboard-data/status/get_200_pending.json new file mode 100644 index 0000000..74dafc6 --- /dev/null +++ b/json/mock_data/dashboard-data/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "dashboard-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/json/mock_data/dashboard-data/status/get_200_progress.json b/json/mock_data/dashboard-data/status/get_200_progress.json new file mode 100644 index 0000000..e3515e5 --- /dev/null +++ b/json/mock_data/dashboard-data/status/get_200_progress.json @@ -0,0 +1,14 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "dashboard-task-123", + "status": "PROGRESS", + "progress": { + "current": 5, + "total": 15, + "card": "sensorValuesList", + "message": "processing sensorValuesList" + } + } +} diff --git a/json/mock_data/dashboard-data/status/get_200_success.json b/json/mock_data/dashboard-data/status/get_200_success.json new file mode 100644 index 0000000..4de043c --- /dev/null +++ b/json/mock_data/dashboard-data/status/get_200_success.json @@ -0,0 +1,41 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "dashboard-task-123", + "status": "SUCCESS", + "result": { + "sensor_id": "550e8400-e29b-41d4-a716-446655440000", + "all_cards": { + "farmOverviewKpis": { + "healthScore": 82, + "activeAlerts": 2, + "waterNeedMm": 18.4 + }, + "sensorValuesList": { + "items": [ + { + "label": "رطوبت خاک", + "value": 45.2, + "unit": "%" + }, + { + "label": "دما خاک", + "value": 22.5, + "unit": "°C" + } + ] + }, + "recommendationsList": { + "items": [ + { + "recommendation_title": "تنظیم نوبت آبیاری", + "suggested_action": "آبیاری بعدی را صبح فردا انجام دهید.", + "urgency_level": "high" + } + ] + } + } + } + } +} diff --git a/json/mock_data/fertilization/recommend/post_202.json b/json/mock_data/fertilization/recommend/post_202.json new file mode 100644 index 0000000..e8dc23f --- /dev/null +++ b/json/mock_data/fertilization/recommend/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "تسک توصیه کودهی در صف قرار گرفت.", + "data": { + "task_id": "fert-task-123", + "status_url": "/api/fertilization/recommend/fert-task-123/status/" + } +} diff --git a/json/mock_data/fertilization/recommend/post_400.json b/json/mock_data/fertilization/recommend/post_400.json new file mode 100644 index 0000000..9fdc597 --- /dev/null +++ b/json/mock_data/fertilization/recommend/post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "sensor_uuid": [ + "This field is required." + ] + } +} diff --git a/json/mock_data/fertilization/status/get_200_failure.json b/json/mock_data/fertilization/status/get_200_failure.json new file mode 100644 index 0000000..fb8bad0 --- /dev/null +++ b/json/mock_data/fertilization/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "fert-task-123", + "status": "FAILURE", + "error": "خطا در دریافت توصیه کودهی." + } +} diff --git a/json/mock_data/fertilization/status/get_200_pending.json b/json/mock_data/fertilization/status/get_200_pending.json new file mode 100644 index 0000000..110be1a --- /dev/null +++ b/json/mock_data/fertilization/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "fert-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/json/mock_data/fertilization/status/get_200_progress.json b/json/mock_data/fertilization/status/get_200_progress.json new file mode 100644 index 0000000..ffdf909 --- /dev/null +++ b/json/mock_data/fertilization/status/get_200_progress.json @@ -0,0 +1,11 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "fert-task-123", + "status": "PROGRESS", + "progress": { + "message": "در حال پردازش توصیه کودهی..." + } + } +} diff --git a/json/mock_data/fertilization/status/get_200_success.json b/json/mock_data/fertilization/status/get_200_success.json new file mode 100644 index 0000000..7b46ba1 --- /dev/null +++ b/json/mock_data/fertilization/status/get_200_success.json @@ -0,0 +1,19 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "fert-task-123", + "status": "SUCCESS", + "result": { + "plan": { + "npkRatio": "20-20-20", + "amountPerHectare": "150 kg/ha", + "applicationMethod": "کودآبیاری در دو نوبت", + "applicationInterval": "هر ۱۰ روز", + "reasoning": "نیتروژن و پتاسیم خاک در محدوده متوسط است و گیاه در فاز رویشی نیاز تغذیه‌ای بالاتری دارد." + }, + "raw_response": "{\"plan\":{\"npkRatio\":\"20-20-20\"}}", + "status": "completed" + } + } +} diff --git a/json/mock_data/index.json b/json/mock_data/index.json new file mode 100644 index 0000000..d051fb6 --- /dev/null +++ b/json/mock_data/index.json @@ -0,0 +1,604 @@ +[ + { + "method": "POST", + "path": "/api/dashboard-data/generate/", + "status_code": 202, + "description": "Dashboard data task queued", + "file": "json/mock_data/dashboard-data/generate/post_202.json" + }, + { + "method": "POST", + "path": "/api/dashboard-data/generate/", + "status_code": 400, + "description": "Missing sensor_id", + "file": "json/mock_data/dashboard-data/generate/post_400.json" + }, + { + "method": "GET", + "path": "/api/dashboard-data/{task_id}/status/", + "status_code": 200, + "description": "Pending dashboard task", + "file": "json/mock_data/dashboard-data/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/dashboard-data/{task_id}/status/", + "status_code": 200, + "description": "Dashboard task in progress", + "file": "json/mock_data/dashboard-data/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/dashboard-data/{task_id}/status/", + "status_code": 200, + "description": "Successful dashboard task", + "file": "json/mock_data/dashboard-data/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/dashboard-data/{task_id}/status/", + "status_code": 200, + "description": "Failed dashboard task", + "file": "json/mock_data/dashboard-data/status/get_200_failure.json" + }, + { + "method": "POST", + "path": "/api/fertilization/recommend/", + "status_code": 202, + "description": "Fertilization task queued", + "file": "json/mock_data/fertilization/recommend/post_202.json" + }, + { + "method": "POST", + "path": "/api/fertilization/recommend/", + "status_code": 400, + "description": "Validation error", + "file": "json/mock_data/fertilization/recommend/post_400.json" + }, + { + "method": "GET", + "path": "/api/fertilization/recommend/{task_id}/status/", + "status_code": 200, + "description": "Fertilization status pending", + "file": "json/mock_data/fertilization/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/fertilization/recommend/{task_id}/status/", + "status_code": 200, + "description": "Fertilization status progress", + "file": "json/mock_data/fertilization/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/fertilization/recommend/{task_id}/status/", + "status_code": 200, + "description": "Fertilization status success", + "file": "json/mock_data/fertilization/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/fertilization/recommend/{task_id}/status/", + "status_code": 200, + "description": "Fertilization status failure", + "file": "json/mock_data/fertilization/status/get_200_failure.json" + }, + { + "method": "GET", + "path": "/api/irrigation/", + "status_code": 200, + "description": "List irrigation methods", + "file": "json/mock_data/irrigation/methods/get_200.json" + }, + { + "method": "POST", + "path": "/api/irrigation/", + "status_code": 201, + "description": "Create irrigation method", + "file": "json/mock_data/irrigation/methods/post_201.json" + }, + { + "method": "POST", + "path": "/api/irrigation/", + "status_code": 400, + "description": "Irrigation create validation error", + "file": "json/mock_data/irrigation/methods/post_400.json" + }, + { + "method": "POST", + "path": "/api/irrigation/recommend/", + "status_code": 202, + "description": "Irrigation recommendation task queued", + "file": "json/mock_data/irrigation/recommend/post_202.json" + }, + { + "method": "POST", + "path": "/api/irrigation/recommend/", + "status_code": 400, + "description": "Irrigation recommendation validation error", + "file": "json/mock_data/irrigation/recommend/post_400.json" + }, + { + "method": "GET", + "path": "/api/irrigation/recommend/{task_id}/status/", + "status_code": 200, + "description": "Irrigation recommendation status pending", + "file": "json/mock_data/irrigation/recommend/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/irrigation/recommend/{task_id}/status/", + "status_code": 200, + "description": "Irrigation recommendation status progress", + "file": "json/mock_data/irrigation/recommend/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/irrigation/recommend/{task_id}/status/", + "status_code": 200, + "description": "Irrigation recommendation status success", + "file": "json/mock_data/irrigation/recommend/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/irrigation/recommend/{task_id}/status/", + "status_code": 200, + "description": "Irrigation recommendation status failure", + "file": "json/mock_data/irrigation/recommend/status/get_200_failure.json" + }, + { + "method": "GET", + "path": "/api/irrigation/{pk}/", + "status_code": 200, + "description": "Irrigation method get success", + "file": "json/mock_data/irrigation/method-detail/get_200.json" + }, + { + "method": "GET", + "path": "/api/irrigation/{pk}/", + "status_code": 404, + "description": "Irrigation method get not found", + "file": "json/mock_data/irrigation/method-detail/get_404.json" + }, + { + "method": "PUT", + "path": "/api/irrigation/{pk}/", + "status_code": 200, + "description": "Irrigation method put success", + "file": "json/mock_data/irrigation/method-detail/put_200.json" + }, + { + "method": "PUT", + "path": "/api/irrigation/{pk}/", + "status_code": 400, + "description": "Irrigation method put validation error", + "file": "json/mock_data/irrigation/method-detail/put_400.json" + }, + { + "method": "PUT", + "path": "/api/irrigation/{pk}/", + "status_code": 404, + "description": "Irrigation method put not found", + "file": "json/mock_data/irrigation/method-detail/put_404.json" + }, + { + "method": "PATCH", + "path": "/api/irrigation/{pk}/", + "status_code": 200, + "description": "Irrigation method patch success", + "file": "json/mock_data/irrigation/method-detail/patch_200.json" + }, + { + "method": "PATCH", + "path": "/api/irrigation/{pk}/", + "status_code": 400, + "description": "Irrigation method patch validation error", + "file": "json/mock_data/irrigation/method-detail/patch_400.json" + }, + { + "method": "PATCH", + "path": "/api/irrigation/{pk}/", + "status_code": 404, + "description": "Irrigation method patch not found", + "file": "json/mock_data/irrigation/method-detail/patch_404.json" + }, + { + "method": "DELETE", + "path": "/api/irrigation/{pk}/", + "status_code": 200, + "description": "Delete irrigation method", + "file": "json/mock_data/irrigation/method-detail/delete_200.json" + }, + { + "method": "DELETE", + "path": "/api/irrigation/{pk}/", + "status_code": 404, + "description": "Delete irrigation method not found", + "file": "json/mock_data/irrigation/method-detail/delete_404.json" + }, + { + "method": "GET", + "path": "/api/soil-data/", + "status_code": 200, + "description": "Soil data served from database", + "file": "json/mock_data/soil-data/get_200_database.json" + }, + { + "method": "GET", + "path": "/api/soil-data/", + "status_code": 202, + "description": "Soil data fetch task queued", + "file": "json/mock_data/soil-data/get_202_queued.json" + }, + { + "method": "GET", + "path": "/api/soil-data/", + "status_code": 400, + "description": "Soil data validation error", + "file": "json/mock_data/soil-data/get_400.json" + }, + { + "method": "POST", + "path": "/api/soil-data/", + "status_code": 200, + "description": "Soil data POST served from database", + "file": "json/mock_data/soil-data/post_200_database.json" + }, + { + "method": "POST", + "path": "/api/soil-data/", + "status_code": 202, + "description": "Soil data POST task queued", + "file": "json/mock_data/soil-data/post_202_queued.json" + }, + { + "method": "POST", + "path": "/api/soil-data/", + "status_code": 400, + "description": "Soil data POST validation error", + "file": "json/mock_data/soil-data/post_400.json" + }, + { + "method": "GET", + "path": "/api/soil-data/tasks/{task_id}/status/", + "status_code": 200, + "description": "Soil task status pending", + "file": "json/mock_data/soil-data/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/soil-data/tasks/{task_id}/status/", + "status_code": 200, + "description": "Soil task status progress", + "file": "json/mock_data/soil-data/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/soil-data/tasks/{task_id}/status/", + "status_code": 200, + "description": "Soil task status success", + "file": "json/mock_data/soil-data/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/soil-data/tasks/{task_id}/status/", + "status_code": 200, + "description": "Soil task status failure", + "file": "json/mock_data/soil-data/status/get_200_failure.json" + }, + { + "method": "GET", + "path": "/api/plants/", + "status_code": 200, + "description": "List plants", + "file": "json/mock_data/plant/list-get_200.json" + }, + { + "method": "POST", + "path": "/api/plants/", + "status_code": 201, + "description": "Create plant", + "file": "json/mock_data/plant/create-post_201.json" + }, + { + "method": "POST", + "path": "/api/plants/", + "status_code": 400, + "description": "Plant create validation error", + "file": "json/mock_data/plant/create-post_400.json" + }, + { + "method": "GET", + "path": "/api/plants/{pk}/", + "status_code": 200, + "description": "Plant detail get success", + "file": "json/mock_data/plant/detail-get_200.json" + }, + { + "method": "GET", + "path": "/api/plants/{pk}/", + "status_code": 404, + "description": "Plant detail get not found", + "file": "json/mock_data/plant/detail-get_404.json" + }, + { + "method": "PUT", + "path": "/api/plants/{pk}/", + "status_code": 200, + "description": "Plant detail put success", + "file": "json/mock_data/plant/detail-put_200.json" + }, + { + "method": "PUT", + "path": "/api/plants/{pk}/", + "status_code": 400, + "description": "Plant detail put validation error", + "file": "json/mock_data/plant/detail-put_400.json" + }, + { + "method": "PUT", + "path": "/api/plants/{pk}/", + "status_code": 404, + "description": "Plant detail put not found", + "file": "json/mock_data/plant/detail-put_404.json" + }, + { + "method": "PATCH", + "path": "/api/plants/{pk}/", + "status_code": 200, + "description": "Plant detail patch success", + "file": "json/mock_data/plant/detail-patch_200.json" + }, + { + "method": "PATCH", + "path": "/api/plants/{pk}/", + "status_code": 400, + "description": "Plant detail patch validation error", + "file": "json/mock_data/plant/detail-patch_400.json" + }, + { + "method": "PATCH", + "path": "/api/plants/{pk}/", + "status_code": 404, + "description": "Plant detail patch not found", + "file": "json/mock_data/plant/detail-patch_404.json" + }, + { + "method": "DELETE", + "path": "/api/plants/{pk}/", + "status_code": 200, + "description": "Delete plant success", + "file": "json/mock_data/plant/detail-delete_200.json" + }, + { + "method": "DELETE", + "path": "/api/plants/{pk}/", + "status_code": 404, + "description": "Delete plant not found", + "file": "json/mock_data/plant/detail-delete_404.json" + }, + { + "method": "POST", + "path": "/api/plants/fetch-info/", + "status_code": 200, + "description": "Fetch plant info success", + "file": "json/mock_data/plant/fetch-info-post_200.json" + }, + { + "method": "POST", + "path": "/api/plants/fetch-info/", + "status_code": 400, + "description": "Fetch plant info missing name", + "file": "json/mock_data/plant/fetch-info-post_400.json" + }, + { + "method": "POST", + "path": "/api/plants/fetch-info/", + "status_code": 503, + "description": "Fetch plant info service unavailable", + "file": "json/mock_data/plant/fetch-info-post_503.json" + }, + { + "method": "POST", + "path": "/api/rag/chat/", + "status_code": 200, + "description": "RAG chat streaming response", + "file": "json/mock_data/rag/chat-post_200_stream.json" + }, + { + "method": "POST", + "path": "/api/rag/chat/", + "status_code": 400, + "description": "Missing query", + "file": "json/mock_data/rag/chat-post_400_missing_query.json" + }, + { + "method": "POST", + "path": "/api/rag/chat/", + "status_code": 400, + "description": "Invalid service id", + "file": "json/mock_data/rag/chat-post_400_invalid_service.json" + }, + { + "method": "POST", + "path": "/api/rag/chat/", + "status_code": 400, + "description": "Missing user_id for service", + "file": "json/mock_data/rag/chat-post_400_missing_user.json" + }, + { + "method": "POST", + "path": "/api/rag/recommend/irrigation/", + "status_code": 202, + "description": "RAG irrigation task queued", + "file": "json/mock_data/rag/irrigation/post_202.json" + }, + { + "method": "POST", + "path": "/api/rag/recommend/irrigation/", + "status_code": 400, + "description": "RAG irrigation validation error", + "file": "json/mock_data/rag/irrigation/post_400.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/irrigation/{task_id}/status/", + "status_code": 200, + "description": "RAG irrigation status pending", + "file": "json/mock_data/rag/irrigation/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/irrigation/{task_id}/status/", + "status_code": 200, + "description": "RAG irrigation status progress", + "file": "json/mock_data/rag/irrigation/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/irrigation/{task_id}/status/", + "status_code": 200, + "description": "RAG irrigation status success", + "file": "json/mock_data/rag/irrigation/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/irrigation/{task_id}/status/", + "status_code": 200, + "description": "RAG irrigation status failure", + "file": "json/mock_data/rag/irrigation/status/get_200_failure.json" + }, + { + "method": "POST", + "path": "/api/rag/recommend/fertilization/", + "status_code": 202, + "description": "RAG fertilization task queued", + "file": "json/mock_data/rag/fertilization/post_202.json" + }, + { + "method": "POST", + "path": "/api/rag/recommend/fertilization/", + "status_code": 400, + "description": "RAG fertilization validation error", + "file": "json/mock_data/rag/fertilization/post_400.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/fertilization/{task_id}/status/", + "status_code": 200, + "description": "RAG fertilization status pending", + "file": "json/mock_data/rag/fertilization/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/fertilization/{task_id}/status/", + "status_code": 200, + "description": "RAG fertilization status progress", + "file": "json/mock_data/rag/fertilization/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/fertilization/{task_id}/status/", + "status_code": 200, + "description": "RAG fertilization status success", + "file": "json/mock_data/rag/fertilization/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/fertilization/{task_id}/status/", + "status_code": 200, + "description": "RAG fertilization status failure", + "file": "json/mock_data/rag/fertilization/status/get_200_failure.json" + }, + { + "method": "PUT", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 200, + "description": "Sensor update put success", + "file": "json/mock_data/sensor-data/update-put_200.json" + }, + { + "method": "PUT", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 400, + "description": "Sensor update put validation error", + "file": "json/mock_data/sensor-data/update-put_400.json" + }, + { + "method": "PUT", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 404, + "description": "Sensor update put location not found", + "file": "json/mock_data/sensor-data/update-put_404.json" + }, + { + "method": "PATCH", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 200, + "description": "Sensor update patch success", + "file": "json/mock_data/sensor-data/update-patch_200.json" + }, + { + "method": "PATCH", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 400, + "description": "Sensor update patch validation error", + "file": "json/mock_data/sensor-data/update-patch_400.json" + }, + { + "method": "PATCH", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 404, + "description": "Sensor update patch location not found", + "file": "json/mock_data/sensor-data/update-patch_404.json" + }, + { + "method": "POST", + "path": "/api/sensor-data/parameters/", + "status_code": 201, + "description": "Create sensor parameter", + "file": "json/mock_data/sensor-data/parameters-post_201.json" + }, + { + "method": "POST", + "path": "/api/sensor-data/parameters/", + "status_code": 400, + "description": "Sensor parameter validation error", + "file": "json/mock_data/sensor-data/parameters-post_400.json" + }, + { + "method": "POST", + "path": "/api/tasks/", + "status_code": 200, + "description": "Task trigger success", + "file": "json/mock_data/tasks/post_200.json" + }, + { + "method": "GET", + "path": "/api/tasks/{task_id}/status/", + "status_code": 200, + "description": "Task status pending", + "file": "json/mock_data/tasks/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/tasks/{task_id}/status/", + "status_code": 200, + "description": "Task status progress", + "file": "json/mock_data/tasks/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/tasks/{task_id}/status/", + "status_code": 200, + "description": "Task status success", + "file": "json/mock_data/tasks/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/tasks/{task_id}/status/", + "status_code": 200, + "description": "Task status failure", + "file": "json/mock_data/tasks/status/get_200_failure.json" + } +] diff --git a/json/mock_data/irrigation/method-detail/delete_200.json b/json/mock_data/irrigation/method-detail/delete_200.json new file mode 100644 index 0000000..ed52092 --- /dev/null +++ b/json/mock_data/irrigation/method-detail/delete_200.json @@ -0,0 +1,5 @@ +{ + "code": 200, + "msg": "روش آبیاری با موفقیت حذف شد.", + "data": null +} diff --git a/json/mock_data/irrigation/method-detail/delete_404.json b/json/mock_data/irrigation/method-detail/delete_404.json new file mode 100644 index 0000000..54dcfff --- /dev/null +++ b/json/mock_data/irrigation/method-detail/delete_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "روش آبیاری یافت نشد.", + "data": null +} diff --git a/json/mock_data/irrigation/method-detail/get_200.json b/json/mock_data/irrigation/method-detail/get_200.json new file mode 100644 index 0000000..5988c67 --- /dev/null +++ b/json/mock_data/irrigation/method-detail/get_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/irrigation/method-detail/get_404.json b/json/mock_data/irrigation/method-detail/get_404.json new file mode 100644 index 0000000..54dcfff --- /dev/null +++ b/json/mock_data/irrigation/method-detail/get_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "روش آبیاری یافت نشد.", + "data": null +} diff --git a/json/mock_data/irrigation/method-detail/patch_200.json b/json/mock_data/irrigation/method-detail/patch_200.json new file mode 100644 index 0000000..5988c67 --- /dev/null +++ b/json/mock_data/irrigation/method-detail/patch_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/irrigation/method-detail/patch_400.json b/json/mock_data/irrigation/method-detail/patch_400.json new file mode 100644 index 0000000..2e4dd22 --- /dev/null +++ b/json/mock_data/irrigation/method-detail/patch_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field may not be blank." + ] + } +} diff --git a/json/mock_data/irrigation/method-detail/patch_404.json b/json/mock_data/irrigation/method-detail/patch_404.json new file mode 100644 index 0000000..54dcfff --- /dev/null +++ b/json/mock_data/irrigation/method-detail/patch_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "روش آبیاری یافت نشد.", + "data": null +} diff --git a/json/mock_data/irrigation/method-detail/put_200.json b/json/mock_data/irrigation/method-detail/put_200.json new file mode 100644 index 0000000..5988c67 --- /dev/null +++ b/json/mock_data/irrigation/method-detail/put_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/irrigation/method-detail/put_400.json b/json/mock_data/irrigation/method-detail/put_400.json new file mode 100644 index 0000000..2e4dd22 --- /dev/null +++ b/json/mock_data/irrigation/method-detail/put_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field may not be blank." + ] + } +} diff --git a/json/mock_data/irrigation/method-detail/put_404.json b/json/mock_data/irrigation/method-detail/put_404.json new file mode 100644 index 0000000..54dcfff --- /dev/null +++ b/json/mock_data/irrigation/method-detail/put_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "روش آبیاری یافت نشد.", + "data": null +} diff --git a/json/mock_data/irrigation/methods/get_200.json b/json/mock_data/irrigation/methods/get_200.json new file mode 100644 index 0000000..a2e511e --- /dev/null +++ b/json/mock_data/irrigation/methods/get_200.json @@ -0,0 +1,20 @@ +{ + "code": 200, + "msg": "success", + "data": [ + { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } + ] +} diff --git a/json/mock_data/irrigation/methods/post_201.json b/json/mock_data/irrigation/methods/post_201.json new file mode 100644 index 0000000..2fba4f5 --- /dev/null +++ b/json/mock_data/irrigation/methods/post_201.json @@ -0,0 +1,18 @@ +{ + "code": 201, + "msg": "success", + "data": { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/irrigation/methods/post_400.json b/json/mock_data/irrigation/methods/post_400.json new file mode 100644 index 0000000..f72f28c --- /dev/null +++ b/json/mock_data/irrigation/methods/post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field is required." + ] + } +} diff --git a/json/mock_data/irrigation/recommend/post_202.json b/json/mock_data/irrigation/recommend/post_202.json new file mode 100644 index 0000000..a5f230d --- /dev/null +++ b/json/mock_data/irrigation/recommend/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "تسک توصیه آبیاری در صف قرار گرفت.", + "data": { + "task_id": "irr-task-123", + "status_url": "/api/irrigation/recommend/irr-task-123/status/" + } +} diff --git a/json/mock_data/irrigation/recommend/post_400.json b/json/mock_data/irrigation/recommend/post_400.json new file mode 100644 index 0000000..9fdc597 --- /dev/null +++ b/json/mock_data/irrigation/recommend/post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "sensor_uuid": [ + "This field is required." + ] + } +} diff --git a/json/mock_data/irrigation/recommend/status/get_200_failure.json b/json/mock_data/irrigation/recommend/status/get_200_failure.json new file mode 100644 index 0000000..5f4e1f7 --- /dev/null +++ b/json/mock_data/irrigation/recommend/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "irr-task-123", + "status": "FAILURE", + "error": "خطا در دریافت توصیه آبیاری." + } +} diff --git a/json/mock_data/irrigation/recommend/status/get_200_pending.json b/json/mock_data/irrigation/recommend/status/get_200_pending.json new file mode 100644 index 0000000..498b382 --- /dev/null +++ b/json/mock_data/irrigation/recommend/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "irr-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/json/mock_data/irrigation/recommend/status/get_200_progress.json b/json/mock_data/irrigation/recommend/status/get_200_progress.json new file mode 100644 index 0000000..e8837dd --- /dev/null +++ b/json/mock_data/irrigation/recommend/status/get_200_progress.json @@ -0,0 +1,11 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "irr-task-123", + "status": "PROGRESS", + "progress": { + "message": "در حال پردازش توصیه آبیاری..." + } + } +} diff --git a/json/mock_data/irrigation/recommend/status/get_200_success.json b/json/mock_data/irrigation/recommend/status/get_200_success.json new file mode 100644 index 0000000..46f9c90 --- /dev/null +++ b/json/mock_data/irrigation/recommend/status/get_200_success.json @@ -0,0 +1,37 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "irr-task-123", + "status": "SUCCESS", + "result": { + "plan": { + "frequencyPerWeek": 3, + "durationMinutes": 42, + "bestTimeOfDay": "صبح زود", + "moistureLevel": 68, + "warning": "در صورت بارش موثر، نوبت سوم این هفته را حذف کنید." + }, + "raw_response": "{\"plan\":{\"frequencyPerWeek\":3,\"durationMinutes\":42}}", + "water_balance": { + "daily": [ + { + "forecast_date": "2025-03-25", + "et0_mm": 4.7, + "etc_mm": 5.6, + "effective_rainfall_mm": 0.0, + "gross_irrigation_mm": 6.2, + "irrigation_timing": "06:00-08:00" + } + ], + "crop_profile": { + "kc_initial": 0.6, + "kc_mid": 1.15, + "kc_end": 0.8 + }, + "active_kc": 1.15 + }, + "status": "completed" + } + } +} diff --git a/json/mock_data/plant/create-post_201.json b/json/mock_data/plant/create-post_201.json new file mode 100644 index 0000000..32c4614 --- /dev/null +++ b/json/mock_data/plant/create-post_201.json @@ -0,0 +1,18 @@ +{ + "code": 201, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/plant/create-post_400.json b/json/mock_data/plant/create-post_400.json new file mode 100644 index 0000000..f72f28c --- /dev/null +++ b/json/mock_data/plant/create-post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field is required." + ] + } +} diff --git a/json/mock_data/plant/detail-delete_200.json b/json/mock_data/plant/detail-delete_200.json new file mode 100644 index 0000000..b127160 --- /dev/null +++ b/json/mock_data/plant/detail-delete_200.json @@ -0,0 +1,5 @@ +{ + "code": 200, + "msg": "گیاه با موفقیت حذف شد.", + "data": null +} diff --git a/json/mock_data/plant/detail-delete_404.json b/json/mock_data/plant/detail-delete_404.json new file mode 100644 index 0000000..497519d --- /dev/null +++ b/json/mock_data/plant/detail-delete_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "گیاه یافت نشد.", + "data": null +} diff --git a/json/mock_data/plant/detail-get_200.json b/json/mock_data/plant/detail-get_200.json new file mode 100644 index 0000000..af5f8ad --- /dev/null +++ b/json/mock_data/plant/detail-get_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/plant/detail-get_404.json b/json/mock_data/plant/detail-get_404.json new file mode 100644 index 0000000..497519d --- /dev/null +++ b/json/mock_data/plant/detail-get_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "گیاه یافت نشد.", + "data": null +} diff --git a/json/mock_data/plant/detail-patch_200.json b/json/mock_data/plant/detail-patch_200.json new file mode 100644 index 0000000..af5f8ad --- /dev/null +++ b/json/mock_data/plant/detail-patch_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/plant/detail-patch_400.json b/json/mock_data/plant/detail-patch_400.json new file mode 100644 index 0000000..2e4dd22 --- /dev/null +++ b/json/mock_data/plant/detail-patch_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field may not be blank." + ] + } +} diff --git a/json/mock_data/plant/detail-patch_404.json b/json/mock_data/plant/detail-patch_404.json new file mode 100644 index 0000000..497519d --- /dev/null +++ b/json/mock_data/plant/detail-patch_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "گیاه یافت نشد.", + "data": null +} diff --git a/json/mock_data/plant/detail-put_200.json b/json/mock_data/plant/detail-put_200.json new file mode 100644 index 0000000..af5f8ad --- /dev/null +++ b/json/mock_data/plant/detail-put_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/plant/detail-put_400.json b/json/mock_data/plant/detail-put_400.json new file mode 100644 index 0000000..2e4dd22 --- /dev/null +++ b/json/mock_data/plant/detail-put_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field may not be blank." + ] + } +} diff --git a/json/mock_data/plant/detail-put_404.json b/json/mock_data/plant/detail-put_404.json new file mode 100644 index 0000000..497519d --- /dev/null +++ b/json/mock_data/plant/detail-put_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "گیاه یافت نشد.", + "data": null +} diff --git a/json/mock_data/plant/fetch-info-post_200.json b/json/mock_data/plant/fetch-info-post_200.json new file mode 100644 index 0000000..af5f8ad --- /dev/null +++ b/json/mock_data/plant/fetch-info-post_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/plant/fetch-info-post_400.json b/json/mock_data/plant/fetch-info-post_400.json new file mode 100644 index 0000000..e4bbdd2 --- /dev/null +++ b/json/mock_data/plant/fetch-info-post_400.json @@ -0,0 +1,5 @@ +{ + "code": 400, + "msg": "نام گیاه الزامی است.", + "data": null +} diff --git a/json/mock_data/plant/fetch-info-post_503.json b/json/mock_data/plant/fetch-info-post_503.json new file mode 100644 index 0000000..f4911a7 --- /dev/null +++ b/json/mock_data/plant/fetch-info-post_503.json @@ -0,0 +1,5 @@ +{ + "code": 503, + "msg": "سرویس API هنوز پیاده‌سازی نشده است.", + "data": null +} diff --git a/json/mock_data/plant/list-get_200.json b/json/mock_data/plant/list-get_200.json new file mode 100644 index 0000000..e6586a2 --- /dev/null +++ b/json/mock_data/plant/list-get_200.json @@ -0,0 +1,20 @@ +{ + "code": 200, + "msg": "success", + "data": [ + { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } + ] +} diff --git a/json/mock_data/rag/chat-post_200_stream.json b/json/mock_data/rag/chat-post_200_stream.json new file mode 100644 index 0000000..ead2147 --- /dev/null +++ b/json/mock_data/rag/chat-post_200_stream.json @@ -0,0 +1,4 @@ +{ + "content_type": "text/plain; charset=utf-8", + "body": "سلام، برای بازیابی رطوبت خاک بهتر است آبیاری صبح‌گاهی را تنظیم کنید." +} diff --git a/json/mock_data/rag/chat-post_400_invalid_service.json b/json/mock_data/rag/chat-post_400_invalid_service.json new file mode 100644 index 0000000..c484816 --- /dev/null +++ b/json/mock_data/rag/chat-post_400_invalid_service.json @@ -0,0 +1,4 @@ +{ + "code": 400, + "msg": "service_id نامعتبر است: unknown_service" +} diff --git a/json/mock_data/rag/chat-post_400_missing_query.json b/json/mock_data/rag/chat-post_400_missing_query.json new file mode 100644 index 0000000..b491272 --- /dev/null +++ b/json/mock_data/rag/chat-post_400_missing_query.json @@ -0,0 +1,4 @@ +{ + "code": 400, + "msg": "پارامتر query الزامی است." +} diff --git a/json/mock_data/rag/chat-post_400_missing_user.json b/json/mock_data/rag/chat-post_400_missing_user.json new file mode 100644 index 0000000..3615785 --- /dev/null +++ b/json/mock_data/rag/chat-post_400_missing_user.json @@ -0,0 +1,4 @@ +{ + "code": 400, + "msg": "برای این service_id، پارامتر user_id الزامی است." +} diff --git a/json/mock_data/rag/fertilization/post_202.json b/json/mock_data/rag/fertilization/post_202.json new file mode 100644 index 0000000..8fe7cc7 --- /dev/null +++ b/json/mock_data/rag/fertilization/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "تسک توصیه کودهی در صف قرار گرفت.", + "data": { + "task_id": "rag-fert-123", + "status_url": "/api/rag/recommend/fertilization/rag-fert-123/status/" + } +} diff --git a/json/mock_data/rag/fertilization/post_400.json b/json/mock_data/rag/fertilization/post_400.json new file mode 100644 index 0000000..6bea070 --- /dev/null +++ b/json/mock_data/rag/fertilization/post_400.json @@ -0,0 +1,5 @@ +{ + "code": 400, + "msg": "پارامتر sensor_uuid الزامی است.", + "data": null +} diff --git a/json/mock_data/rag/fertilization/status/get_200_failure.json b/json/mock_data/rag/fertilization/status/get_200_failure.json new file mode 100644 index 0000000..971dd04 --- /dev/null +++ b/json/mock_data/rag/fertilization/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-fert-123", + "status": "FAILURE", + "error": "خطا در دریافت توصیه کودهی." + } +} diff --git a/json/mock_data/rag/fertilization/status/get_200_pending.json b/json/mock_data/rag/fertilization/status/get_200_pending.json new file mode 100644 index 0000000..9c9eca6 --- /dev/null +++ b/json/mock_data/rag/fertilization/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-fert-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/json/mock_data/rag/fertilization/status/get_200_progress.json b/json/mock_data/rag/fertilization/status/get_200_progress.json new file mode 100644 index 0000000..4cd6d57 --- /dev/null +++ b/json/mock_data/rag/fertilization/status/get_200_progress.json @@ -0,0 +1,11 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-fert-123", + "status": "PROGRESS", + "progress": { + "message": "در حال پردازش توصیه کودهی..." + } + } +} diff --git a/json/mock_data/rag/fertilization/status/get_200_success.json b/json/mock_data/rag/fertilization/status/get_200_success.json new file mode 100644 index 0000000..cea132a --- /dev/null +++ b/json/mock_data/rag/fertilization/status/get_200_success.json @@ -0,0 +1,19 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-fert-123", + "status": "SUCCESS", + "result": { + "plan": { + "npkRatio": "20-20-20", + "amountPerHectare": "150 kg/ha", + "applicationMethod": "کودآبیاری در دو نوبت", + "applicationInterval": "هر ۱۰ روز", + "reasoning": "نیتروژن و پتاسیم خاک در محدوده متوسط است و گیاه در فاز رویشی نیاز تغذیه‌ای بالاتری دارد." + }, + "raw_response": "{\"plan\":{\"npkRatio\":\"20-20-20\"}}", + "status": "completed" + } + } +} diff --git a/json/mock_data/rag/irrigation/post_202.json b/json/mock_data/rag/irrigation/post_202.json new file mode 100644 index 0000000..bcc6db9 --- /dev/null +++ b/json/mock_data/rag/irrigation/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "تسک توصیه آبیاری در صف قرار گرفت.", + "data": { + "task_id": "rag-irr-123", + "status_url": "/api/rag/recommend/irrigation/rag-irr-123/status/" + } +} diff --git a/json/mock_data/rag/irrigation/post_400.json b/json/mock_data/rag/irrigation/post_400.json new file mode 100644 index 0000000..6bea070 --- /dev/null +++ b/json/mock_data/rag/irrigation/post_400.json @@ -0,0 +1,5 @@ +{ + "code": 400, + "msg": "پارامتر sensor_uuid الزامی است.", + "data": null +} diff --git a/json/mock_data/rag/irrigation/status/get_200_failure.json b/json/mock_data/rag/irrigation/status/get_200_failure.json new file mode 100644 index 0000000..bffa098 --- /dev/null +++ b/json/mock_data/rag/irrigation/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-irr-123", + "status": "FAILURE", + "error": "خطا در دریافت توصیه آبیاری." + } +} diff --git a/json/mock_data/rag/irrigation/status/get_200_pending.json b/json/mock_data/rag/irrigation/status/get_200_pending.json new file mode 100644 index 0000000..1074e86 --- /dev/null +++ b/json/mock_data/rag/irrigation/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-irr-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/json/mock_data/rag/irrigation/status/get_200_progress.json b/json/mock_data/rag/irrigation/status/get_200_progress.json new file mode 100644 index 0000000..3b88990 --- /dev/null +++ b/json/mock_data/rag/irrigation/status/get_200_progress.json @@ -0,0 +1,11 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-irr-123", + "status": "PROGRESS", + "progress": { + "message": "در حال پردازش توصیه آبیاری..." + } + } +} diff --git a/json/mock_data/rag/irrigation/status/get_200_success.json b/json/mock_data/rag/irrigation/status/get_200_success.json new file mode 100644 index 0000000..5679648 --- /dev/null +++ b/json/mock_data/rag/irrigation/status/get_200_success.json @@ -0,0 +1,37 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-irr-123", + "status": "SUCCESS", + "result": { + "plan": { + "frequencyPerWeek": 3, + "durationMinutes": 42, + "bestTimeOfDay": "صبح زود", + "moistureLevel": 68, + "warning": "در صورت بارش موثر، نوبت سوم این هفته را حذف کنید." + }, + "raw_response": "{\"plan\":{\"frequencyPerWeek\":3,\"durationMinutes\":42}}", + "water_balance": { + "daily": [ + { + "forecast_date": "2025-03-25", + "et0_mm": 4.7, + "etc_mm": 5.6, + "effective_rainfall_mm": 0.0, + "gross_irrigation_mm": 6.2, + "irrigation_timing": "06:00-08:00" + } + ], + "crop_profile": { + "kc_initial": 0.6, + "kc_mid": 1.15, + "kc_end": 0.8 + }, + "active_kc": 1.15 + }, + "status": "completed" + } + } +} diff --git a/json/mock_data/sensor-data/parameters-post_201.json b/json/mock_data/sensor-data/parameters-post_201.json new file mode 100644 index 0000000..9de5e9d --- /dev/null +++ b/json/mock_data/sensor-data/parameters-post_201.json @@ -0,0 +1,12 @@ +{ + "code": 201, + "msg": "success", + "data": { + "id": 3, + "code": "soil_moisture", + "name_fa": "رطوبت خاک", + "unit": "%", + "created_at": "2025-03-24T10:00:00Z", + "action": "added" + } +} diff --git a/json/mock_data/sensor-data/parameters-post_400.json b/json/mock_data/sensor-data/parameters-post_400.json new file mode 100644 index 0000000..98b89aa --- /dev/null +++ b/json/mock_data/sensor-data/parameters-post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "code": [ + "This field is required." + ] + } +} diff --git a/json/mock_data/sensor-data/update-patch_200.json b/json/mock_data/sensor-data/update-patch_200.json new file mode 100644 index 0000000..5bc26e3 --- /dev/null +++ b/json/mock_data/sensor-data/update-patch_200.json @@ -0,0 +1,20 @@ +{ + "code": 200, + "msg": "success", + "data": { + "uuid_sensor": "550e8400-e29b-41d4-a716-446655440000", + "location_id": 12, + "soil_moisture": 45.2, + "soil_temperature": 22.5, + "soil_ph": 6.8, + "electrical_conductivity": 1.2, + "nitrogen": 30.0, + "phosphorus": 15.0, + "potassium": 20.0, + "plant_ids": [ + 1 + ], + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/sensor-data/update-patch_400.json b/json/mock_data/sensor-data/update-patch_400.json new file mode 100644 index 0000000..cf343c5 --- /dev/null +++ b/json/mock_data/sensor-data/update-patch_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "location_id": [ + "This field is required." + ] + } +} diff --git a/json/mock_data/sensor-data/update-patch_404.json b/json/mock_data/sensor-data/update-patch_404.json new file mode 100644 index 0000000..107ea33 --- /dev/null +++ b/json/mock_data/sensor-data/update-patch_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "location_id یافت نشد.", + "data": null +} diff --git a/json/mock_data/sensor-data/update-put_200.json b/json/mock_data/sensor-data/update-put_200.json new file mode 100644 index 0000000..5bc26e3 --- /dev/null +++ b/json/mock_data/sensor-data/update-put_200.json @@ -0,0 +1,20 @@ +{ + "code": 200, + "msg": "success", + "data": { + "uuid_sensor": "550e8400-e29b-41d4-a716-446655440000", + "location_id": 12, + "soil_moisture": 45.2, + "soil_temperature": 22.5, + "soil_ph": 6.8, + "electrical_conductivity": 1.2, + "nitrogen": 30.0, + "phosphorus": 15.0, + "potassium": 20.0, + "plant_ids": [ + 1 + ], + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/json/mock_data/sensor-data/update-put_400.json b/json/mock_data/sensor-data/update-put_400.json new file mode 100644 index 0000000..cf343c5 --- /dev/null +++ b/json/mock_data/sensor-data/update-put_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "location_id": [ + "This field is required." + ] + } +} diff --git a/json/mock_data/sensor-data/update-put_404.json b/json/mock_data/sensor-data/update-put_404.json new file mode 100644 index 0000000..107ea33 --- /dev/null +++ b/json/mock_data/sensor-data/update-put_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "location_id یافت نشد.", + "data": null +} diff --git a/json/mock_data/soil-data/get_200_database.json b/json/mock_data/soil-data/get_200_database.json new file mode 100644 index 0000000..87becb1 --- /dev/null +++ b/json/mock_data/soil-data/get_200_database.json @@ -0,0 +1,63 @@ +{ + "code": 200, + "msg": "success", + "data": { + "source": "database", + "id": 12, + "lon": "51.389000", + "lat": "35.689200", + "depths": [ + { + "depth_label": "0-5cm", + "bdod": 1.31, + "cec": 18.4, + "cfvo": 2.0, + "clay": 24.0, + "nitrogen": 0.18, + "ocd": 32.0, + "ocs": 4.1, + "phh2o": 7.2, + "sand": 34.0, + "silt": 42.0, + "soc": 1.6, + "wv0010": 0.31, + "wv0033": 0.22, + "wv1500": 0.11 + }, + { + "depth_label": "5-15cm", + "bdod": 1.35, + "cec": 17.2, + "cfvo": 2.3, + "clay": 26.0, + "nitrogen": 0.16, + "ocd": 28.0, + "ocs": 3.7, + "phh2o": 7.1, + "sand": 36.0, + "silt": 38.0, + "soc": 1.4, + "wv0010": 0.29, + "wv0033": 0.2, + "wv1500": 0.1 + }, + { + "depth_label": "15-30cm", + "bdod": 1.39, + "cec": 15.8, + "cfvo": 2.8, + "clay": 28.0, + "nitrogen": 0.13, + "ocd": 22.0, + "ocs": 3.2, + "phh2o": 7.0, + "sand": 38.0, + "silt": 34.0, + "soc": 1.1, + "wv0010": 0.26, + "wv0033": 0.18, + "wv1500": 0.09 + } + ] + } +} diff --git a/json/mock_data/soil-data/get_202_queued.json b/json/mock_data/soil-data/get_202_queued.json new file mode 100644 index 0000000..5a06843 --- /dev/null +++ b/json/mock_data/soil-data/get_202_queued.json @@ -0,0 +1,11 @@ +{ + "code": 202, + "msg": "تسک در صف. وضعیت را با task_id بررسی کنید.", + "data": { + "source": "task", + "task_id": "soil-task-123", + "lon": 51.389, + "lat": 35.6892, + "status_url": "/api/soil-data/tasks/soil-task-123/status/" + } +} diff --git a/json/mock_data/soil-data/get_400.json b/json/mock_data/soil-data/get_400.json new file mode 100644 index 0000000..bfc19ab --- /dev/null +++ b/json/mock_data/soil-data/get_400.json @@ -0,0 +1,12 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "lat": [ + "This field is required." + ], + "lon": [ + "This field is required." + ] + } +} diff --git a/json/mock_data/soil-data/post_200_database.json b/json/mock_data/soil-data/post_200_database.json new file mode 100644 index 0000000..87becb1 --- /dev/null +++ b/json/mock_data/soil-data/post_200_database.json @@ -0,0 +1,63 @@ +{ + "code": 200, + "msg": "success", + "data": { + "source": "database", + "id": 12, + "lon": "51.389000", + "lat": "35.689200", + "depths": [ + { + "depth_label": "0-5cm", + "bdod": 1.31, + "cec": 18.4, + "cfvo": 2.0, + "clay": 24.0, + "nitrogen": 0.18, + "ocd": 32.0, + "ocs": 4.1, + "phh2o": 7.2, + "sand": 34.0, + "silt": 42.0, + "soc": 1.6, + "wv0010": 0.31, + "wv0033": 0.22, + "wv1500": 0.11 + }, + { + "depth_label": "5-15cm", + "bdod": 1.35, + "cec": 17.2, + "cfvo": 2.3, + "clay": 26.0, + "nitrogen": 0.16, + "ocd": 28.0, + "ocs": 3.7, + "phh2o": 7.1, + "sand": 36.0, + "silt": 38.0, + "soc": 1.4, + "wv0010": 0.29, + "wv0033": 0.2, + "wv1500": 0.1 + }, + { + "depth_label": "15-30cm", + "bdod": 1.39, + "cec": 15.8, + "cfvo": 2.8, + "clay": 28.0, + "nitrogen": 0.13, + "ocd": 22.0, + "ocs": 3.2, + "phh2o": 7.0, + "sand": 38.0, + "silt": 34.0, + "soc": 1.1, + "wv0010": 0.26, + "wv0033": 0.18, + "wv1500": 0.09 + } + ] + } +} diff --git a/json/mock_data/soil-data/post_202_queued.json b/json/mock_data/soil-data/post_202_queued.json new file mode 100644 index 0000000..5a06843 --- /dev/null +++ b/json/mock_data/soil-data/post_202_queued.json @@ -0,0 +1,11 @@ +{ + "code": 202, + "msg": "تسک در صف. وضعیت را با task_id بررسی کنید.", + "data": { + "source": "task", + "task_id": "soil-task-123", + "lon": 51.389, + "lat": 35.6892, + "status_url": "/api/soil-data/tasks/soil-task-123/status/" + } +} diff --git a/json/mock_data/soil-data/post_400.json b/json/mock_data/soil-data/post_400.json new file mode 100644 index 0000000..4d32daf --- /dev/null +++ b/json/mock_data/soil-data/post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "lat": [ + "A valid number is required." + ] + } +} diff --git a/json/mock_data/soil-data/status/get_200_failure.json b/json/mock_data/soil-data/status/get_200_failure.json new file mode 100644 index 0000000..a0c0697 --- /dev/null +++ b/json/mock_data/soil-data/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "soil-task-123", + "status": "FAILURE", + "error": "خطا در واکشی داده خاک." + } +} diff --git a/json/mock_data/soil-data/status/get_200_pending.json b/json/mock_data/soil-data/status/get_200_pending.json new file mode 100644 index 0000000..b4965ce --- /dev/null +++ b/json/mock_data/soil-data/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "soil-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/json/mock_data/soil-data/status/get_200_progress.json b/json/mock_data/soil-data/status/get_200_progress.json new file mode 100644 index 0000000..29ecfe5 --- /dev/null +++ b/json/mock_data/soil-data/status/get_200_progress.json @@ -0,0 +1,12 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "soil-task-123", + "status": "PROGRESS", + "progress": { + "step": "fetch", + "percent": 60 + } + } +} diff --git a/json/mock_data/soil-data/status/get_200_success.json b/json/mock_data/soil-data/status/get_200_success.json new file mode 100644 index 0000000..04d0827 --- /dev/null +++ b/json/mock_data/soil-data/status/get_200_success.json @@ -0,0 +1,67 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "soil-task-123", + "status": "SUCCESS", + "result": { + "source": "database", + "id": 12, + "lon": "51.389000", + "lat": "35.689200", + "depths": [ + { + "depth_label": "0-5cm", + "bdod": 1.31, + "cec": 18.4, + "cfvo": 2.0, + "clay": 24.0, + "nitrogen": 0.18, + "ocd": 32.0, + "ocs": 4.1, + "phh2o": 7.2, + "sand": 34.0, + "silt": 42.0, + "soc": 1.6, + "wv0010": 0.31, + "wv0033": 0.22, + "wv1500": 0.11 + }, + { + "depth_label": "5-15cm", + "bdod": 1.35, + "cec": 17.2, + "cfvo": 2.3, + "clay": 26.0, + "nitrogen": 0.16, + "ocd": 28.0, + "ocs": 3.7, + "phh2o": 7.1, + "sand": 36.0, + "silt": 38.0, + "soc": 1.4, + "wv0010": 0.29, + "wv0033": 0.2, + "wv1500": 0.1 + }, + { + "depth_label": "15-30cm", + "bdod": 1.39, + "cec": 15.8, + "cfvo": 2.8, + "clay": 28.0, + "nitrogen": 0.13, + "ocd": 22.0, + "ocs": 3.2, + "phh2o": 7.0, + "sand": 38.0, + "silt": 34.0, + "soc": 1.1, + "wv0010": 0.26, + "wv0033": 0.18, + "wv1500": 0.09 + } + ] + } + } +} diff --git a/json/mock_data/tasks/post_200.json b/json/mock_data/tasks/post_200.json new file mode 100644 index 0000000..8fc9fe2 --- /dev/null +++ b/json/mock_data/tasks/post_200.json @@ -0,0 +1,7 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123" + } +} diff --git a/json/mock_data/tasks/status/get_200_failure.json b/json/mock_data/tasks/status/get_200_failure.json new file mode 100644 index 0000000..2c34884 --- /dev/null +++ b/json/mock_data/tasks/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123", + "status": "FAILURE", + "error": "Sample task failed." + } +} diff --git a/json/mock_data/tasks/status/get_200_pending.json b/json/mock_data/tasks/status/get_200_pending.json new file mode 100644 index 0000000..c6ce7cb --- /dev/null +++ b/json/mock_data/tasks/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/json/mock_data/tasks/status/get_200_progress.json b/json/mock_data/tasks/status/get_200_progress.json new file mode 100644 index 0000000..abb1b76 --- /dev/null +++ b/json/mock_data/tasks/status/get_200_progress.json @@ -0,0 +1,13 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123", + "status": "PROGRESS", + "progress": { + "current": 1, + "total": 3, + "message": "در حال پردازش..." + } + } +} diff --git a/json/mock_data/tasks/status/get_200_success.json b/json/mock_data/tasks/status/get_200_success.json new file mode 100644 index 0000000..5b55965 --- /dev/null +++ b/json/mock_data/tasks/status/get_200_success.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123", + "status": "SUCCESS", + "result": "done" + } +} diff --git a/location_data/serializers.py b/location_data/serializers.py index 829320e..13a6f9b 100644 --- a/location_data/serializers.py +++ b/location_data/serializers.py @@ -73,4 +73,4 @@ class SoilDataTaskResponseSerializer(serializers.Serializer): task_id = serializers.CharField() lon = serializers.FloatField(source="longitude") lat = serializers.FloatField(source="latitude") - status_url = serializers.URLField(required=False) + status_url = serializers.CharField(required=False) diff --git a/location_data/views.py b/location_data/views.py index 876aea9..50d574d 100644 --- a/location_data/views.py +++ b/location_data/views.py @@ -9,15 +9,50 @@ 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 خاک: مختصات جغرافیایی را می‌گیرد. @@ -49,9 +84,18 @@ class SoilDataView(APIView): }, ], responses={ - 200: OpenApiResponse(description="داده خاک از دیتابیس"), - 202: OpenApiResponse(description="تسک در صف قرار گرفت"), - 400: OpenApiResponse(description="داده نامعتبر"), + 200: build_response( + SoilDataResponseSerializer, + "داده خاک از دیتابیس بازگردانده شد.", + ), + 202: build_response( + SoilTaskQueuedResponseSerializer, + "تسک واکشی داده خاک در صف قرار گرفت.", + ), + 400: build_response( + SoilErrorResponseSerializer, + "پارامترهای ورودی نامعتبر هستند.", + ), }, ) def get(self, request): @@ -63,9 +107,18 @@ class SoilDataView(APIView): description="با ارسال lat و lon در بدنه، داده خاک از DB یا از طریق تسک Celery برگردانده می‌شود.", request=SoilDataRequestSerializer, responses={ - 200: OpenApiResponse(description="داده خاک از دیتابیس"), - 202: OpenApiResponse(description="تسک در صف قرار گرفت"), - 400: OpenApiResponse(description="داده نامعتبر"), + 200: build_response( + SoilDataResponseSerializer, + "داده خاک از دیتابیس بازگردانده شد.", + ), + 202: build_response( + SoilTaskQueuedResponseSerializer, + "تسک واکشی داده خاک در صف قرار گرفت.", + ), + 400: build_response( + SoilErrorResponseSerializer, + "پارامترهای ورودی نامعتبر هستند.", + ), }, examples=[ OpenApiExample( @@ -141,23 +194,9 @@ class SoilDataTaskStatusView(APIView): summary="وضعیت تسک داده خاک", description="وضعیت تسک Celery واکشی داده خاک را برمی‌گرداند.", responses={ - 200: inline_serializer( - name="SoilTaskStatusResponse", - fields={ - "code": drf_serializers.IntegerField(), - "msg": drf_serializers.CharField(), - "data": inline_serializer( - name="SoilTaskStatusData", - fields={ - "task_id": drf_serializers.CharField(), - "status": drf_serializers.CharField(), - "message": drf_serializers.CharField(required=False), - "progress": drf_serializers.DictField(required=False), - "result": drf_serializers.JSONField(required=False), - "error": drf_serializers.CharField(required=False), - }, - ), - }, + 200: build_response( + SoilTaskStatusResponseSerializer, + "وضعیت فعلی تسک واکشی داده خاک.", ), }, ) diff --git a/plant/views.py b/plant/views.py index 6d17200..fbb73fd 100644 --- a/plant/views.py +++ b/plant/views.py @@ -9,11 +9,32 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView +from config.openapi import build_envelope_serializer, build_response from .models import Plant from .serializers import PlantSerializer from .services import fetch_plant_info_from_api +PlantListResponseSerializer = build_envelope_serializer( + "PlantListResponseSerializer", + PlantSerializer, + many=True, +) +PlantDetailResponseSerializer = build_envelope_serializer( + "PlantDetailResponseSerializer", + PlantSerializer, +) +PlantValidationErrorSerializer = build_envelope_serializer( + "PlantValidationErrorSerializer", + data_required=False, + allow_null=True, +) +PlantFetchInfoResponseSerializer = build_envelope_serializer( + "PlantFetchInfoResponseSerializer", + PlantSerializer, +) + + class PlantListCreateView(APIView): """لیست تمام گیاهان و ایجاد گیاه جدید.""" @@ -21,7 +42,12 @@ class PlantListCreateView(APIView): tags=["Plant"], summary="لیست گیاهان", description="لیست تمام گیاهان ذخیره‌شده را برمی‌گرداند.", - responses={200: PlantSerializer(many=True)}, + responses={ + 200: build_response( + PlantListResponseSerializer, + "لیست گیاهان ذخیره‌شده.", + ), + }, ) def get(self, request): plants = Plant.objects.all() @@ -37,8 +63,14 @@ class PlantListCreateView(APIView): description="یک گیاه جدید با مشخصات داده‌شده ایجاد می‌کند.", request=PlantSerializer, responses={ - 201: PlantSerializer, - 400: OpenApiResponse(description="داده نامعتبر"), + 201: build_response( + PlantDetailResponseSerializer, + "گیاه جدید با موفقیت ایجاد شد.", + ), + 400: build_response( + PlantValidationErrorSerializer, + "داده ورودی نامعتبر است.", + ), }, examples=[ OpenApiExample( @@ -83,8 +115,14 @@ class PlantDetailView(APIView): summary="جزئیات گیاه", description="مشخصات یک گیاه را بر اساس شناسه برمی‌گرداند.", responses={ - 200: PlantSerializer, - 404: OpenApiResponse(description="گیاه یافت نشد"), + 200: build_response( + PlantDetailResponseSerializer, + "جزئیات گیاه.", + ), + 404: build_response( + PlantValidationErrorSerializer, + "گیاه یافت نشد.", + ), }, ) def get(self, request, pk): @@ -106,9 +144,18 @@ class PlantDetailView(APIView): description="تمام فیلدهای یک گیاه را آپدیت می‌کند.", request=PlantSerializer, responses={ - 200: PlantSerializer, - 400: OpenApiResponse(description="داده نامعتبر"), - 404: OpenApiResponse(description="گیاه یافت نشد"), + 200: build_response( + PlantDetailResponseSerializer, + "گیاه با موفقیت به‌روزرسانی شد.", + ), + 400: build_response( + PlantValidationErrorSerializer, + "داده ورودی نامعتبر است.", + ), + 404: build_response( + PlantValidationErrorSerializer, + "گیاه یافت نشد.", + ), }, ) def put(self, request, pk): @@ -136,9 +183,18 @@ class PlantDetailView(APIView): description="فقط فیلدهای ارسال‌شده آپدیت می‌شوند.", request=PlantSerializer, responses={ - 200: PlantSerializer, - 400: OpenApiResponse(description="داده نامعتبر"), - 404: OpenApiResponse(description="گیاه یافت نشد"), + 200: build_response( + PlantDetailResponseSerializer, + "گیاه با موفقیت به‌روزرسانی شد.", + ), + 400: build_response( + PlantValidationErrorSerializer, + "داده ورودی نامعتبر است.", + ), + 404: build_response( + PlantValidationErrorSerializer, + "گیاه یافت نشد.", + ), }, ) def patch(self, request, pk): @@ -165,8 +221,14 @@ class PlantDetailView(APIView): summary="حذف گیاه", description="یک گیاه را حذف می‌کند.", responses={ - 200: OpenApiResponse(description="حذف موفق"), - 404: OpenApiResponse(description="گیاه یافت نشد"), + 200: build_response( + PlantValidationErrorSerializer, + "گیاه با موفقیت حذف شد.", + ), + 404: build_response( + PlantValidationErrorSerializer, + "گیاه یافت نشد.", + ), }, ) def delete(self, request, pk): @@ -197,9 +259,18 @@ class PlantFetchInfoView(APIView): }, ), responses={ - 200: PlantSerializer, - 400: OpenApiResponse(description="نام گیاه ارسال نشده"), - 503: OpenApiResponse(description="سرویس در دسترس نیست"), + 200: build_response( + PlantFetchInfoResponseSerializer, + "اطلاعات گیاه از سرویس خارجی دریافت شد.", + ), + 400: build_response( + PlantValidationErrorSerializer, + "نام گیاه ارسال نشده است.", + ), + 503: build_response( + PlantValidationErrorSerializer, + "سرویس خارجی در دسترس نیست.", + ), }, examples=[ OpenApiExample( diff --git a/rag/views.py b/rag/views.py index ad4ec0a..8f0d7c5 100644 --- a/rag/views.py +++ b/rag/views.py @@ -1,7 +1,10 @@ """ ویوهای RAG — چت با استریم """ +import logging + from django.http import StreamingHttpResponse +from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( OpenApiExample, OpenApiResponse, @@ -13,14 +16,46 @@ from rest_framework import serializers as drf_serializers from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -import logging +from config.openapi import ( + build_envelope_serializer, + build_message_response_serializer, + build_response, + build_task_queue_data_serializer, + build_task_status_data_serializer, +) from .chat import chat_rag_stream logger = logging.getLogger(__name__) +RagChatErrorResponseSerializer = build_message_response_serializer( + "RagChatErrorResponseSerializer" +) +RagIrrigationQueueResponseSerializer = build_envelope_serializer( + "RagIrrigationQueueResponseSerializer", + build_task_queue_data_serializer("RagIrrigationQueueDataSerializer"), +) +RagIrrigationStatusResponseSerializer = build_envelope_serializer( + "RagIrrigationStatusResponseSerializer", + build_task_status_data_serializer("RagIrrigationStatusDataSerializer"), +) +RagFertilizationQueueResponseSerializer = build_envelope_serializer( + "RagFertilizationQueueResponseSerializer", + build_task_queue_data_serializer("RagFertilizationQueueDataSerializer"), +) +RagFertilizationStatusResponseSerializer = build_envelope_serializer( + "RagFertilizationStatusResponseSerializer", + build_task_status_data_serializer("RagFertilizationStatusDataSerializer"), +) +RagValidationErrorResponseSerializer = build_envelope_serializer( + "RagValidationErrorResponseSerializer", + data_required=False, + allow_null=True, +) + + class ChatView(APIView): """ چت RAG با استریم. @@ -44,10 +79,12 @@ class ChatView(APIView): ), responses={ 200: OpenApiResponse( + response=OpenApiTypes.STR, description="پاسخ استریم متنی (text/plain)", ), - 400: OpenApiResponse( - description="پارامتر ورودی نامعتبر", + 400: build_response( + RagChatErrorResponseSerializer, + "پارامترهای ورودی نامعتبر هستند.", ), }, examples=[ @@ -152,21 +189,14 @@ class IrrigationRecommendationView(APIView): }, ), responses={ - 202: inline_serializer( - name="IrrigationRecommendationResponse", - fields={ - "code": drf_serializers.IntegerField(), - "msg": drf_serializers.CharField(), - "data": inline_serializer( - name="IrrigationRecommendationData", - fields={ - "task_id": drf_serializers.CharField(), - "status_url": drf_serializers.CharField(), - }, - ), - }, + 202: build_response( + RagIrrigationQueueResponseSerializer, + "تسک توصیه آبیاری در صف قرار گرفت.", + ), + 400: build_response( + RagValidationErrorResponseSerializer, + "پارامتر ورودی نامعتبر است.", ), - 400: OpenApiResponse(description="پارامتر ورودی نامعتبر"), }, examples=[ OpenApiExample( @@ -219,22 +249,9 @@ class IrrigationRecommendationStatusView(APIView): summary="وضعیت تسک توصیه آبیاری", description="وضعیت تسک Celery توصیه آبیاری را برمی‌گرداند.", responses={ - 200: inline_serializer( - name="IrrigationRecommendationStatusResponse", - fields={ - "code": drf_serializers.IntegerField(), - "msg": drf_serializers.CharField(), - "data": inline_serializer( - name="IrrigationRecommendationStatusData", - fields={ - "task_id": drf_serializers.CharField(), - "status": drf_serializers.CharField(), - "result": drf_serializers.JSONField(required=False), - "progress": drf_serializers.DictField(required=False), - "error": drf_serializers.CharField(required=False), - }, - ), - }, + 200: build_response( + RagIrrigationStatusResponseSerializer, + "وضعیت فعلی تسک توصیه آبیاری.", ), }, ) @@ -281,21 +298,14 @@ class FertilizationRecommendationView(APIView): }, ), responses={ - 202: inline_serializer( - name="FertilizationRecommendationResponse", - fields={ - "code": drf_serializers.IntegerField(), - "msg": drf_serializers.CharField(), - "data": inline_serializer( - name="FertilizationRecommendationData", - fields={ - "task_id": drf_serializers.CharField(), - "status_url": drf_serializers.CharField(), - }, - ), - }, + 202: build_response( + RagFertilizationQueueResponseSerializer, + "تسک توصیه کودهی در صف قرار گرفت.", + ), + 400: build_response( + RagValidationErrorResponseSerializer, + "پارامتر ورودی نامعتبر است.", ), - 400: OpenApiResponse(description="پارامتر ورودی نامعتبر"), }, examples=[ OpenApiExample( @@ -346,22 +356,9 @@ class FertilizationRecommendationStatusView(APIView): summary="وضعیت تسک توصیه کودهی", description="وضعیت تسک Celery توصیه کودهی را برمی‌گرداند.", responses={ - 200: inline_serializer( - name="FertilizationRecommendationStatusResponse", - fields={ - "code": drf_serializers.IntegerField(), - "msg": drf_serializers.CharField(), - "data": inline_serializer( - name="FertilizationRecommendationStatusData", - fields={ - "task_id": drf_serializers.CharField(), - "status": drf_serializers.CharField(), - "result": drf_serializers.JSONField(required=False), - "progress": drf_serializers.DictField(required=False), - "error": drf_serializers.CharField(required=False), - }, - ), - }, + 200: build_response( + RagFertilizationStatusResponseSerializer, + "وضعیت فعلی تسک توصیه کودهی.", ), }, ) diff --git a/scripts/generate_mock_data.py b/scripts/generate_mock_data.py new file mode 100644 index 0000000..ebe4523 --- /dev/null +++ b/scripts/generate_mock_data.py @@ -0,0 +1,829 @@ +import json +from pathlib import Path + + +BASE_DIR = Path(__file__).resolve().parents[1] +MOCK_DIR = BASE_DIR / "json" / "mock_data" + + +def queue_response(message, task_id, status_url, **extra): + data = { + "task_id": task_id, + "status_url": status_url, + } + data.update(extra) + return { + "code": 202, + "msg": message, + "data": data, + } + + +def ok_response(data, message="success", code=200): + return { + "code": code, + "msg": message, + "data": data, + } + + +def error_response(code, message, data=None): + return { + "code": code, + "msg": message, + "data": data, + } + + +def task_pending(task_id): + return ok_response( + { + "task_id": task_id, + "status": "PENDING", + "message": "تسک در صف یا یافت نشد.", + } + ) + + +def task_progress(task_id, progress): + return ok_response( + { + "task_id": task_id, + "status": "PROGRESS", + "progress": progress, + } + ) + + +def task_success(task_id, result): + return ok_response( + { + "task_id": task_id, + "status": "SUCCESS", + "result": result, + } + ) + + +def task_failure(task_id, error): + return ok_response( + { + "task_id": task_id, + "status": "FAILURE", + "error": error, + } + ) + + +PLANT = { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z", +} + +IRRIGATION_METHOD = { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z", +} + +SOIL_DEPTHS = [ + { + "depth_label": "0-5cm", + "bdod": 1.31, + "cec": 18.4, + "cfvo": 2.0, + "clay": 24.0, + "nitrogen": 0.18, + "ocd": 32.0, + "ocs": 4.1, + "phh2o": 7.2, + "sand": 34.0, + "silt": 42.0, + "soc": 1.6, + "wv0010": 0.31, + "wv0033": 0.22, + "wv1500": 0.11, + }, + { + "depth_label": "5-15cm", + "bdod": 1.35, + "cec": 17.2, + "cfvo": 2.3, + "clay": 26.0, + "nitrogen": 0.16, + "ocd": 28.0, + "ocs": 3.7, + "phh2o": 7.1, + "sand": 36.0, + "silt": 38.0, + "soc": 1.4, + "wv0010": 0.29, + "wv0033": 0.2, + "wv1500": 0.1, + }, + { + "depth_label": "15-30cm", + "bdod": 1.39, + "cec": 15.8, + "cfvo": 2.8, + "clay": 28.0, + "nitrogen": 0.13, + "ocd": 22.0, + "ocs": 3.2, + "phh2o": 7.0, + "sand": 38.0, + "silt": 34.0, + "soc": 1.1, + "wv0010": 0.26, + "wv0033": 0.18, + "wv1500": 0.09, + }, +] + +SOIL_LOCATION = { + "source": "database", + "id": 12, + "lon": "51.389000", + "lat": "35.689200", + "depths": SOIL_DEPTHS, +} + +SOIL_TASK_DATA = { + "source": "task", + "task_id": "soil-task-123", + "lon": 51.389, + "lat": 35.6892, + "status_url": "/api/soil-data/tasks/soil-task-123/status/", +} + +SENSOR_DATA = { + "uuid_sensor": "550e8400-e29b-41d4-a716-446655440000", + "location_id": 12, + "soil_moisture": 45.2, + "soil_temperature": 22.5, + "soil_ph": 6.8, + "electrical_conductivity": 1.2, + "nitrogen": 30.0, + "phosphorus": 15.0, + "potassium": 20.0, + "plant_ids": [1], + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z", +} + +SENSOR_PARAMETER = { + "id": 3, + "code": "soil_moisture", + "name_fa": "رطوبت خاک", + "unit": "%", + "created_at": "2025-03-24T10:00:00Z", + "action": "added", +} + +IRRIGATION_RECOMMENDATION_RESULT = { + "plan": { + "frequencyPerWeek": 3, + "durationMinutes": 42, + "bestTimeOfDay": "صبح زود", + "moistureLevel": 68, + "warning": "در صورت بارش موثر، نوبت سوم این هفته را حذف کنید.", + }, + "raw_response": "{\"plan\":{\"frequencyPerWeek\":3,\"durationMinutes\":42}}", + "water_balance": { + "daily": [ + { + "forecast_date": "2025-03-25", + "et0_mm": 4.7, + "etc_mm": 5.6, + "effective_rainfall_mm": 0.0, + "gross_irrigation_mm": 6.2, + "irrigation_timing": "06:00-08:00", + } + ], + "crop_profile": { + "kc_initial": 0.6, + "kc_mid": 1.15, + "kc_end": 0.8, + }, + "active_kc": 1.15, + }, + "status": "completed", +} + +FERTILIZATION_RECOMMENDATION_RESULT = { + "plan": { + "npkRatio": "20-20-20", + "amountPerHectare": "150 kg/ha", + "applicationMethod": "کودآبیاری در دو نوبت", + "applicationInterval": "هر ۱۰ روز", + "reasoning": "نیتروژن و پتاسیم خاک در محدوده متوسط است و گیاه در فاز رویشی نیاز تغذیه‌ای بالاتری دارد.", + }, + "raw_response": "{\"plan\":{\"npkRatio\":\"20-20-20\"}}", + "status": "completed", +} + +DASHBOARD_RESULT = { + "sensor_id": "550e8400-e29b-41d4-a716-446655440000", + "all_cards": { + "farmOverviewKpis": { + "healthScore": 82, + "activeAlerts": 2, + "waterNeedMm": 18.4, + }, + "sensorValuesList": { + "items": [ + {"label": "رطوبت خاک", "value": 45.2, "unit": "%"}, + {"label": "دما خاک", "value": 22.5, "unit": "°C"}, + ] + }, + "recommendationsList": { + "items": [ + { + "recommendation_title": "تنظیم نوبت آبیاری", + "suggested_action": "آبیاری بعدی را صبح فردا انجام دهید.", + "urgency_level": "high", + } + ] + }, + }, +} + +TASK_PROGRESS = {"current": 1, "total": 3, "message": "در حال پردازش..."} + +RAG_STREAM_SUCCESS = { + "content_type": "text/plain; charset=utf-8", + "body": "سلام، برای بازیابی رطوبت خاک بهتر است آبیاری صبح‌گاهی را تنظیم کنید.", +} + +index = [] + + +def register(path, method, api_path, status_code, description, payload): + full_path = MOCK_DIR / path + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + index.append( + { + "method": method, + "path": api_path, + "status_code": status_code, + "description": description, + "file": str(Path("json/mock_data") / path), + } + ) + + +def main(): + register( + "dashboard-data/generate/post_202.json", + "POST", + "/api/dashboard-data/generate/", + 202, + "Dashboard data task queued", + queue_response( + "dashboard task queued", + "dashboard-task-123", + "/api/dashboard-data/dashboard-task-123/status/", + ), + ) + register( + "dashboard-data/generate/post_400.json", + "POST", + "/api/dashboard-data/generate/", + 400, + "Missing sensor_id", + error_response(400, "پارامتر sensor_id الزامی است.", None), + ) + register( + "dashboard-data/status/get_200_pending.json", + "GET", + "/api/dashboard-data/{task_id}/status/", + 200, + "Pending dashboard task", + task_pending("dashboard-task-123"), + ) + register( + "dashboard-data/status/get_200_progress.json", + "GET", + "/api/dashboard-data/{task_id}/status/", + 200, + "Dashboard task in progress", + task_progress( + "dashboard-task-123", + {"current": 5, "total": 15, "card": "sensorValuesList", "message": "processing sensorValuesList"}, + ), + ) + register( + "dashboard-data/status/get_200_success.json", + "GET", + "/api/dashboard-data/{task_id}/status/", + 200, + "Successful dashboard task", + task_success("dashboard-task-123", DASHBOARD_RESULT), + ) + register( + "dashboard-data/status/get_200_failure.json", + "GET", + "/api/dashboard-data/{task_id}/status/", + 200, + "Failed dashboard task", + task_failure("dashboard-task-123", "خطا در ساخت کارت‌های داشبورد."), + ) + + register( + "fertilization/recommend/post_202.json", + "POST", + "/api/fertilization/recommend/", + 202, + "Fertilization task queued", + queue_response( + "تسک توصیه کودهی در صف قرار گرفت.", + "fert-task-123", + "/api/fertilization/recommend/fert-task-123/status/", + ), + ) + register( + "fertilization/recommend/post_400.json", + "POST", + "/api/fertilization/recommend/", + 400, + "Validation error", + error_response(400, "داده نامعتبر.", {"sensor_uuid": ["This field is required."]}), + ) + for name, payload in { + "pending": task_pending("fert-task-123"), + "progress": task_progress("fert-task-123", {"message": "در حال پردازش توصیه کودهی..."}), + "success": task_success("fert-task-123", FERTILIZATION_RECOMMENDATION_RESULT), + "failure": task_failure("fert-task-123", "خطا در دریافت توصیه کودهی."), + }.items(): + register( + f"fertilization/status/get_200_{name}.json", + "GET", + "/api/fertilization/recommend/{task_id}/status/", + 200, + f"Fertilization status {name}", + payload, + ) + + register( + "irrigation/methods/get_200.json", + "GET", + "/api/irrigation/", + 200, + "List irrigation methods", + ok_response([IRRIGATION_METHOD]), + ) + register( + "irrigation/methods/post_201.json", + "POST", + "/api/irrigation/", + 201, + "Create irrigation method", + ok_response(IRRIGATION_METHOD, code=201), + ) + register( + "irrigation/methods/post_400.json", + "POST", + "/api/irrigation/", + 400, + "Irrigation create validation error", + error_response(400, "داده نامعتبر.", {"name": ["This field is required."]}), + ) + register( + "irrigation/recommend/post_202.json", + "POST", + "/api/irrigation/recommend/", + 202, + "Irrigation recommendation task queued", + queue_response( + "تسک توصیه آبیاری در صف قرار گرفت.", + "irr-task-123", + "/api/irrigation/recommend/irr-task-123/status/", + ), + ) + register( + "irrigation/recommend/post_400.json", + "POST", + "/api/irrigation/recommend/", + 400, + "Irrigation recommendation validation error", + error_response(400, "داده نامعتبر.", {"sensor_uuid": ["This field is required."]}), + ) + for name, payload in { + "pending": task_pending("irr-task-123"), + "progress": task_progress("irr-task-123", {"message": "در حال پردازش توصیه آبیاری..."}), + "success": task_success("irr-task-123", IRRIGATION_RECOMMENDATION_RESULT), + "failure": task_failure("irr-task-123", "خطا در دریافت توصیه آبیاری."), + }.items(): + register( + f"irrigation/recommend/status/get_200_{name}.json", + "GET", + "/api/irrigation/recommend/{task_id}/status/", + 200, + f"Irrigation recommendation status {name}", + payload, + ) + + for method, success_code in (("get", 200), ("put", 200), ("patch", 200)): + register( + f"irrigation/method-detail/{method}_{success_code}.json", + method.upper(), + "/api/irrigation/{pk}/", + success_code, + f"Irrigation method {method} success", + ok_response(IRRIGATION_METHOD, code=success_code), + ) + if method in {"put", "patch"}: + register( + f"irrigation/method-detail/{method}_400.json", + method.upper(), + "/api/irrigation/{pk}/", + 400, + f"Irrigation method {method} validation error", + error_response(400, "داده نامعتبر.", {"name": ["This field may not be blank."]}), + ) + register( + f"irrigation/method-detail/{method}_404.json", + method.upper(), + "/api/irrigation/{pk}/", + 404, + f"Irrigation method {method} not found", + error_response(404, "روش آبیاری یافت نشد.", None), + ) + register( + "irrigation/method-detail/delete_200.json", + "DELETE", + "/api/irrigation/{pk}/", + 200, + "Delete irrigation method", + error_response(200, "روش آبیاری با موفقیت حذف شد.", None), + ) + register( + "irrigation/method-detail/delete_404.json", + "DELETE", + "/api/irrigation/{pk}/", + 404, + "Delete irrigation method not found", + error_response(404, "روش آبیاری یافت نشد.", None), + ) + + register( + "soil-data/get_200_database.json", + "GET", + "/api/soil-data/", + 200, + "Soil data served from database", + ok_response(SOIL_LOCATION), + ) + register( + "soil-data/get_202_queued.json", + "GET", + "/api/soil-data/", + 202, + "Soil data fetch task queued", + { + "code": 202, + "msg": "تسک در صف. وضعیت را با task_id بررسی کنید.", + "data": SOIL_TASK_DATA, + }, + ) + register( + "soil-data/get_400.json", + "GET", + "/api/soil-data/", + 400, + "Soil data validation error", + error_response(400, "داده نامعتبر.", {"lat": ["This field is required."], "lon": ["This field is required."]}), + ) + register( + "soil-data/post_200_database.json", + "POST", + "/api/soil-data/", + 200, + "Soil data POST served from database", + ok_response(SOIL_LOCATION), + ) + register( + "soil-data/post_202_queued.json", + "POST", + "/api/soil-data/", + 202, + "Soil data POST task queued", + { + "code": 202, + "msg": "تسک در صف. وضعیت را با task_id بررسی کنید.", + "data": SOIL_TASK_DATA, + }, + ) + register( + "soil-data/post_400.json", + "POST", + "/api/soil-data/", + 400, + "Soil data POST validation error", + error_response(400, "داده نامعتبر.", {"lat": ["A valid number is required."]}), + ) + for name, payload in { + "pending": task_pending("soil-task-123"), + "progress": task_progress("soil-task-123", {"step": "fetch", "percent": 60}), + "success": task_success("soil-task-123", SOIL_LOCATION | {"source": "database"}), + "failure": task_failure("soil-task-123", "خطا در واکشی داده خاک."), + }.items(): + register( + f"soil-data/status/get_200_{name}.json", + "GET", + "/api/soil-data/tasks/{task_id}/status/", + 200, + f"Soil task status {name}", + payload, + ) + + register( + "plant/list-get_200.json", + "GET", + "/api/plants/", + 200, + "List plants", + ok_response([PLANT]), + ) + register( + "plant/create-post_201.json", + "POST", + "/api/plants/", + 201, + "Create plant", + ok_response(PLANT, code=201), + ) + register( + "plant/create-post_400.json", + "POST", + "/api/plants/", + 400, + "Plant create validation error", + error_response(400, "داده نامعتبر.", {"name": ["This field is required."]}), + ) + for method in ("get", "put", "patch"): + register( + f"plant/detail-{method}_200.json", + method.upper(), + "/api/plants/{pk}/", + 200, + f"Plant detail {method} success", + ok_response(PLANT), + ) + if method in {"put", "patch"}: + register( + f"plant/detail-{method}_400.json", + method.upper(), + "/api/plants/{pk}/", + 400, + f"Plant detail {method} validation error", + error_response(400, "داده نامعتبر.", {"name": ["This field may not be blank."]}), + ) + register( + f"plant/detail-{method}_404.json", + method.upper(), + "/api/plants/{pk}/", + 404, + f"Plant detail {method} not found", + error_response(404, "گیاه یافت نشد.", None), + ) + register( + "plant/detail-delete_200.json", + "DELETE", + "/api/plants/{pk}/", + 200, + "Delete plant success", + error_response(200, "گیاه با موفقیت حذف شد.", None), + ) + register( + "plant/detail-delete_404.json", + "DELETE", + "/api/plants/{pk}/", + 404, + "Delete plant not found", + error_response(404, "گیاه یافت نشد.", None), + ) + register( + "plant/fetch-info-post_200.json", + "POST", + "/api/plants/fetch-info/", + 200, + "Fetch plant info success", + ok_response(PLANT), + ) + register( + "plant/fetch-info-post_400.json", + "POST", + "/api/plants/fetch-info/", + 400, + "Fetch plant info missing name", + error_response(400, "نام گیاه الزامی است.", None), + ) + register( + "plant/fetch-info-post_503.json", + "POST", + "/api/plants/fetch-info/", + 503, + "Fetch plant info service unavailable", + error_response(503, "سرویس API هنوز پیاده‌سازی نشده است.", None), + ) + + register( + "rag/chat-post_200_stream.json", + "POST", + "/api/rag/chat/", + 200, + "RAG chat streaming response", + RAG_STREAM_SUCCESS, + ) + register( + "rag/chat-post_400_missing_query.json", + "POST", + "/api/rag/chat/", + 400, + "Missing query", + {"code": 400, "msg": "پارامتر query الزامی است."}, + ) + register( + "rag/chat-post_400_invalid_service.json", + "POST", + "/api/rag/chat/", + 400, + "Invalid service id", + {"code": 400, "msg": "service_id نامعتبر است: unknown_service"}, + ) + register( + "rag/chat-post_400_missing_user.json", + "POST", + "/api/rag/chat/", + 400, + "Missing user_id for service", + {"code": 400, "msg": "برای این service_id، پارامتر user_id الزامی است."}, + ) + + register( + "rag/irrigation/post_202.json", + "POST", + "/api/rag/recommend/irrigation/", + 202, + "RAG irrigation task queued", + queue_response( + "تسک توصیه آبیاری در صف قرار گرفت.", + "rag-irr-123", + "/api/rag/recommend/irrigation/rag-irr-123/status/", + ), + ) + register( + "rag/irrigation/post_400.json", + "POST", + "/api/rag/recommend/irrigation/", + 400, + "RAG irrigation validation error", + error_response(400, "پارامتر sensor_uuid الزامی است.", None), + ) + for name, payload in { + "pending": task_pending("rag-irr-123"), + "progress": task_progress("rag-irr-123", {"message": "در حال پردازش توصیه آبیاری..."}), + "success": task_success("rag-irr-123", IRRIGATION_RECOMMENDATION_RESULT), + "failure": task_failure("rag-irr-123", "خطا در دریافت توصیه آبیاری."), + }.items(): + register( + f"rag/irrigation/status/get_200_{name}.json", + "GET", + "/api/rag/recommend/irrigation/{task_id}/status/", + 200, + f"RAG irrigation status {name}", + payload, + ) + + register( + "rag/fertilization/post_202.json", + "POST", + "/api/rag/recommend/fertilization/", + 202, + "RAG fertilization task queued", + queue_response( + "تسک توصیه کودهی در صف قرار گرفت.", + "rag-fert-123", + "/api/rag/recommend/fertilization/rag-fert-123/status/", + ), + ) + register( + "rag/fertilization/post_400.json", + "POST", + "/api/rag/recommend/fertilization/", + 400, + "RAG fertilization validation error", + error_response(400, "پارامتر sensor_uuid الزامی است.", None), + ) + for name, payload in { + "pending": task_pending("rag-fert-123"), + "progress": task_progress("rag-fert-123", {"message": "در حال پردازش توصیه کودهی..."}), + "success": task_success("rag-fert-123", FERTILIZATION_RECOMMENDATION_RESULT), + "failure": task_failure("rag-fert-123", "خطا در دریافت توصیه کودهی."), + }.items(): + register( + f"rag/fertilization/status/get_200_{name}.json", + "GET", + "/api/rag/recommend/fertilization/{task_id}/status/", + 200, + f"RAG fertilization status {name}", + payload, + ) + + for method in ("put", "patch"): + register( + f"sensor-data/update-{method}_200.json", + method.upper(), + "/api/sensor-data/{uuid_sensor}/", + 200, + f"Sensor update {method} success", + ok_response(SENSOR_DATA), + ) + register( + f"sensor-data/update-{method}_400.json", + method.upper(), + "/api/sensor-data/{uuid_sensor}/", + 400, + f"Sensor update {method} validation error", + error_response(400, "داده نامعتبر.", {"location_id": ["This field is required."]}), + ) + register( + f"sensor-data/update-{method}_404.json", + method.upper(), + "/api/sensor-data/{uuid_sensor}/", + 404, + f"Sensor update {method} location not found", + error_response(404, "location_id یافت نشد.", None), + ) + register( + "sensor-data/parameters-post_201.json", + "POST", + "/api/sensor-data/parameters/", + 201, + "Create sensor parameter", + ok_response(SENSOR_PARAMETER, code=201), + ) + register( + "sensor-data/parameters-post_400.json", + "POST", + "/api/sensor-data/parameters/", + 400, + "Sensor parameter validation error", + error_response(400, "داده نامعتبر.", {"code": ["This field is required."]}), + ) + + register( + "tasks/post_200.json", + "POST", + "/api/tasks/", + 200, + "Task trigger success", + ok_response({"task_id": "sample-task-123"}), + ) + for name, payload in { + "pending": task_pending("sample-task-123"), + "progress": task_progress("sample-task-123", TASK_PROGRESS), + "success": task_success("sample-task-123", "done"), + "failure": task_failure("sample-task-123", "Sample task failed."), + }.items(): + register( + f"tasks/status/get_200_{name}.json", + "GET", + "/api/tasks/{task_id}/status/", + 200, + f"Task status {name}", + payload, + ) + + (MOCK_DIR / "index.json").write_text( + json.dumps(index, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + +if __name__ == "__main__": + main() diff --git a/sensor_data/views.py b/sensor_data/views.py index 3b75682..634f009 100644 --- a/sensor_data/views.py +++ b/sensor_data/views.py @@ -10,6 +10,7 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView +from config.openapi import build_envelope_serializer, build_response from location_data.models import SoilLocation from .models import ParameterUpdateLog, SensorData, SensorDataHistory, SensorParameter @@ -20,6 +21,36 @@ from .serializers import ( ) +SensorDataEnvelopeSerializer = build_envelope_serializer( + "SensorDataEnvelopeSerializer", + SensorDataResponseSerializer, +) +SensorDataValidationErrorSerializer = build_envelope_serializer( + "SensorDataValidationErrorSerializer", + data_required=False, + allow_null=True, +) +SensorDataNotFoundSerializer = build_envelope_serializer( + "SensorDataNotFoundSerializer", + data_required=False, + allow_null=True, +) +SensorParameterResponseSerializer = build_envelope_serializer( + "SensorParameterEnvelopeSerializer", + inline_serializer( + name="SensorParameterPayloadSerializer", + fields={ + "id": drf_serializers.IntegerField(), + "code": drf_serializers.CharField(), + "name_fa": drf_serializers.CharField(), + "unit": drf_serializers.CharField(), + "created_at": drf_serializers.DateTimeField(), + "action": drf_serializers.CharField(), + }, + ), +) + + class SensorDataUpdateView(APIView): """ آپدیت داده سنسور. هنگام آپدیت، نسخه فعلی در SensorDataHistory ذخیره می‌شود. @@ -31,9 +62,18 @@ class SensorDataUpdateView(APIView): description="داده سنسور را بر اساس uuid_sensor آپدیت (یا ایجاد) می‌کند. نسخه قبلی در تاریخچه ذخیره می‌شود.", request=SensorDataUpdateSerializer, responses={ - 200: SensorDataResponseSerializer, - 400: OpenApiResponse(description="داده نامعتبر"), - 404: OpenApiResponse(description="location_id یافت نشد"), + 200: build_response( + SensorDataEnvelopeSerializer, + "داده سنسور با موفقیت ایجاد یا به‌روزرسانی شد.", + ), + 400: build_response( + SensorDataValidationErrorSerializer, + "داده ورودی نامعتبر است.", + ), + 404: build_response( + SensorDataNotFoundSerializer, + "location_id یافت نشد.", + ), }, examples=[ OpenApiExample( @@ -61,9 +101,18 @@ class SensorDataUpdateView(APIView): description="فقط فیلدهای ارسال‌شده آپدیت می‌شوند.", request=SensorDataUpdateSerializer, responses={ - 200: SensorDataResponseSerializer, - 400: OpenApiResponse(description="داده نامعتبر"), - 404: OpenApiResponse(description="location_id یافت نشد"), + 200: build_response( + SensorDataEnvelopeSerializer, + "داده سنسور با موفقیت به‌روزرسانی شد.", + ), + 400: build_response( + SensorDataValidationErrorSerializer, + "داده ورودی نامعتبر است.", + ), + 404: build_response( + SensorDataNotFoundSerializer, + "location_id یافت نشد.", + ), }, ) def patch(self, request, uuid_sensor): @@ -137,25 +186,14 @@ class SensorParameterCreateView(APIView): description="پارامتر جدید اضافه یا پارامتر موجود را ویرایش می‌کند و در لاگ ثبت می‌شود.", request=SensorParameterSerializer, responses={ - 201: inline_serializer( - name="SensorParameterResponse", - fields={ - "code": drf_serializers.IntegerField(), - "msg": drf_serializers.CharField(), - "data": inline_serializer( - name="SensorParameterData", - fields={ - "id": drf_serializers.IntegerField(), - "code": drf_serializers.CharField(), - "name_fa": drf_serializers.CharField(), - "unit": drf_serializers.CharField(), - "created_at": drf_serializers.DateTimeField(), - "action": drf_serializers.CharField(), - }, - ), - }, + 201: build_response( + SensorParameterResponseSerializer, + "پارامتر سنسور با موفقیت ایجاد یا ویرایش شد.", + ), + 400: build_response( + SensorDataValidationErrorSerializer, + "داده ورودی نامعتبر است.", ), - 400: OpenApiResponse(description="داده نامعتبر"), }, examples=[ OpenApiExample( diff --git a/tasks/views.py b/tasks/views.py index 78c6565..0a7d889 100644 --- a/tasks/views.py +++ b/tasks/views.py @@ -11,9 +11,30 @@ 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 .celery_tasks import sample_task +TaskTriggerResponseSerializer = build_envelope_serializer( + "TaskTriggerResponseSerializer", + inline_serializer( + name="TaskTriggerPayloadSerializer", + fields={ + "task_id": drf_serializers.CharField(), + }, + ), +) +TaskStatusResponseSerializer = build_envelope_serializer( + "TaskStatusEnvelopeSerializer", + build_task_status_data_serializer("TaskStatusPayloadSerializer"), +) + + class TaskTriggerView(APIView): """ ثبت و اجرای تسک. @@ -33,18 +54,9 @@ class TaskTriggerView(APIView): }, ), responses={ - 200: inline_serializer( - name="TaskTriggerResponse", - fields={ - "code": drf_serializers.IntegerField(), - "msg": drf_serializers.CharField(), - "data": inline_serializer( - name="TaskTriggerData", - fields={ - "task_id": drf_serializers.CharField(), - }, - ), - }, + 200: build_response( + TaskTriggerResponseSerializer, + "تسک نمونه با موفقیت در صف قرار گرفت.", ), }, examples=[ @@ -85,23 +97,9 @@ class TaskStatusView(APIView): summary="وضعیت تسک", description="وضعیت یک تسک Celery را بر اساس task_id برمی‌گرداند.", responses={ - 200: inline_serializer( - name="TaskStatusResponse", - fields={ - "code": drf_serializers.IntegerField(), - "msg": drf_serializers.CharField(), - "data": inline_serializer( - name="TaskStatusData", - fields={ - "task_id": drf_serializers.CharField(), - "status": drf_serializers.CharField(), - "message": drf_serializers.CharField(required=False), - "progress": drf_serializers.DictField(required=False), - "result": drf_serializers.JSONField(required=False), - "error": drf_serializers.CharField(required=False), - }, - ), - }, + 200: build_response( + TaskStatusResponseSerializer, + "وضعیت فعلی تسک Celery.", ), }, examples=[