This commit is contained in:
2026-03-25 01:56:41 +03:30
parent eb34360345
commit 98406cfd59
99 changed files with 3156 additions and 232 deletions
+103
View File
@@ -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)
+5
View File
@@ -122,10 +122,15 @@ SPECTACULAR_SETTINGS = {
"TAGS": [ "TAGS": [
{"name": "Dashboard Data", "description": "تجمیع داده‌های داشبورد مزرعه"}, {"name": "Dashboard Data", "description": "تجمیع داده‌های داشبورد مزرعه"},
{"name": "RAG Chat", "description": "چت هوشمند RAG"}, {"name": "RAG Chat", "description": "چت هوشمند RAG"},
{"name": "RAG Recommendations", "description": "توصیه‌های آبیاری و کودهی مبتنی بر RAG"},
{"name": "Tasks", "description": "مدیریت تسک‌های Celery"}, {"name": "Tasks", "description": "مدیریت تسک‌های Celery"},
{"name": "Soil Data", "description": "داده‌های خاک (SoilGrids)"}, {"name": "Soil Data", "description": "داده‌های خاک (SoilGrids)"},
{"name": "Sensor Data", "description": "داده‌های سنسور"}, {"name": "Sensor Data", "description": "داده‌های سنسور"},
{"name": "Sensor Parameters", "description": "پارامترهای سنسور"}, {"name": "Sensor Parameters", "description": "پارامترهای سنسور"},
{"name": "Plant", "description": "مدیریت گیاهان و دریافت اطلاعات گیاه"},
{"name": "Irrigation", "description": "مدیریت روش‌های آبیاری"},
{"name": "Irrigation Recommendation", "description": "درخواست و پیگیری توصیه آبیاری"},
{"name": "Fertilization Recommendation", "description": "درخواست و پیگیری توصیه کودهی"},
], ],
} }
+37 -13
View File
@@ -7,9 +7,34 @@ from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView 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 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): class DashboardDataGenerateView(APIView):
@extend_schema( @extend_schema(
tags=["Dashboard Data"], tags=["Dashboard Data"],
@@ -22,21 +47,14 @@ class DashboardDataGenerateView(APIView):
}, },
), ),
responses={ responses={
202: inline_serializer( 202: build_response(
name="DashboardDataGenerateResponse", DashboardDataGenerateResponseSerializer,
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(),
},
), ),
}, 400: build_response(
DashboardDataErrorResponseSerializer,
"پارامتر ورودی نامعتبر است.",
), ),
400: OpenApiResponse(description="Invalid input"),
}, },
) )
def post(self, request): def post(self, request):
@@ -72,6 +90,12 @@ class DashboardDataStatusView(APIView):
@extend_schema( @extend_schema(
tags=["Dashboard Data"], tags=["Dashboard Data"],
summary="Dashboard task status", summary="Dashboard task status",
responses={
200: build_response(
DashboardDataStatusResponseSerializer,
"وضعیت فعلی تسک داده داشبورد.",
),
},
) )
def get(self, request, task_id): def get(self, request, task_id):
result = AsyncResult(task_id) result = AsyncResult(task_id)
+34 -3
View File
@@ -7,9 +7,31 @@ from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView 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 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): class FertilizationRecommendView(APIView):
""" """
توصیه کودهی با Celery. توصیه کودهی با Celery.
@@ -29,8 +51,14 @@ class FertilizationRecommendView(APIView):
), ),
request=FertilizationRecommendRequestSerializer, request=FertilizationRecommendRequestSerializer,
responses={ responses={
202: OpenApiResponse(description="تسک در صف قرار گرفت"), 202: build_response(
400: OpenApiResponse(description="پارامتر ورودی نامعتبر"), FertilizationQueueResponseSerializer,
"تسک توصیه کودهی در صف قرار گرفت.",
),
400: build_response(
FertilizationValidationErrorSerializer,
"پارامتر ورودی نامعتبر است.",
),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -87,7 +115,10 @@ class FertilizationRecommendStatusView(APIView):
summary="وضعیت تسک توصیه کودهی", summary="وضعیت تسک توصیه کودهی",
description="وضعیت تسک Celery توصیه کودهی را برمی‌گرداند.", description="وضعیت تسک Celery توصیه کودهی را برمی‌گرداند.",
responses={ responses={
200: OpenApiResponse(description="وضعیت تسک"), 200: build_response(
FertilizationStatusResponseSerializer,
"وضعیت فعلی تسک توصیه کودهی.",
),
}, },
) )
def get(self, request, task_id): def get(self, request, task_id):
+136 -59
View File
@@ -1,12 +1,15 @@
from drf_spectacular.utils import ( from drf_spectacular.utils import OpenApiExample, extend_schema
OpenApiExample,
OpenApiResponse,
extend_schema,
)
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView 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 .models import IrrigationMethod
from .serializers import ( from .serializers import (
IrrigationMethodSerializer, 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): class IrrigationMethodListCreateView(APIView):
"""لیست تمام روش‌های آبیاری و ایجاد روش جدید.""" """لیست تمام روش‌های آبیاری و ایجاد روش جدید."""
@@ -21,7 +48,12 @@ class IrrigationMethodListCreateView(APIView):
tags=["Irrigation"], tags=["Irrigation"],
summary="لیست روش‌های آبیاری", summary="لیست روش‌های آبیاری",
description="لیست تمام روش‌های آبیاری ذخیره‌شده را برمی‌گرداند.", description="لیست تمام روش‌های آبیاری ذخیره‌شده را برمی‌گرداند.",
responses={200: IrrigationMethodSerializer(many=True)}, responses={
200: build_response(
IrrigationMethodListResponseSerializer,
"لیست روش‌های آبیاری ذخیره‌شده.",
),
},
) )
def get(self, request): def get(self, request):
methods = IrrigationMethod.objects.all() methods = IrrigationMethod.objects.all()
@@ -31,6 +63,52 @@ class IrrigationMethodListCreateView(APIView):
status=status.HTTP_200_OK, 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): class IrrigationRecommendView(APIView):
""" """
@@ -50,8 +128,14 @@ class IrrigationRecommendView(APIView):
), ),
request=IrrigationRecommendRequestSerializer, request=IrrigationRecommendRequestSerializer,
responses={ responses={
202: OpenApiResponse(description="تسک در صف قرار گرفت"), 202: build_response(
400: OpenApiResponse(description="پارامتر ورودی نامعتبر"), IrrigationQueueResponseSerializer,
"تسک توصیه آبیاری در صف قرار گرفت.",
),
400: build_response(
IrrigationValidationErrorSerializer,
"پارامتر ورودی نامعتبر است.",
),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -111,7 +195,10 @@ class IrrigationRecommendStatusView(APIView):
summary="وضعیت تسک توصیه آبیاری", summary="وضعیت تسک توصیه آبیاری",
description="وضعیت تسک Celery توصیه آبیاری را برمی‌گرداند.", description="وضعیت تسک Celery توصیه آبیاری را برمی‌گرداند.",
responses={ responses={
200: OpenApiResponse(description="وضعیت تسک"), 200: build_response(
IrrigationStatusResponseSerializer,
"وضعیت فعلی تسک توصیه آبیاری.",
),
}, },
) )
def get(self, request, task_id): def get(self, request, task_id):
@@ -132,46 +219,6 @@ class IrrigationRecommendStatusView(APIView):
status=status.HTTP_200_OK, 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): class IrrigationMethodDetailView(APIView):
"""دریافت، ویرایش و حذف یک روش آبیاری.""" """دریافت، ویرایش و حذف یک روش آبیاری."""
@@ -184,8 +231,14 @@ class IrrigationMethodDetailView(APIView):
summary="جزئیات روش آبیاری", summary="جزئیات روش آبیاری",
description="مشخصات یک روش آبیاری را بر اساس شناسه برمی‌گرداند.", description="مشخصات یک روش آبیاری را بر اساس شناسه برمی‌گرداند.",
responses={ responses={
200: IrrigationMethodSerializer, 200: build_response(
404: OpenApiResponse(description="روش آبیاری یافت نشد"), IrrigationMethodDetailResponseSerializer,
"جزئیات روش آبیاری.",
),
404: build_response(
IrrigationValidationErrorSerializer,
"روش آبیاری یافت نشد.",
),
}, },
) )
def get(self, request, pk): def get(self, request, pk):
@@ -207,9 +260,18 @@ class IrrigationMethodDetailView(APIView):
description="تمام فیلدهای یک روش آبیاری را آپدیت می‌کند.", description="تمام فیلدهای یک روش آبیاری را آپدیت می‌کند.",
request=IrrigationMethodSerializer, request=IrrigationMethodSerializer,
responses={ responses={
200: IrrigationMethodSerializer, 200: build_response(
400: OpenApiResponse(description="داده نامعتبر"), IrrigationMethodDetailResponseSerializer,
404: OpenApiResponse(description="روش آبیاری یافت نشد"), "روش آبیاری با موفقیت به‌روزرسانی شد.",
),
400: build_response(
IrrigationValidationErrorSerializer,
"داده ورودی نامعتبر است.",
),
404: build_response(
IrrigationValidationErrorSerializer,
"روش آبیاری یافت نشد.",
),
}, },
) )
def put(self, request, pk): def put(self, request, pk):
@@ -237,9 +299,18 @@ class IrrigationMethodDetailView(APIView):
description="فقط فیلدهای ارسال‌شده آپدیت می‌شوند.", description="فقط فیلدهای ارسال‌شده آپدیت می‌شوند.",
request=IrrigationMethodSerializer, request=IrrigationMethodSerializer,
responses={ responses={
200: IrrigationMethodSerializer, 200: build_response(
400: OpenApiResponse(description="داده نامعتبر"), IrrigationMethodDetailResponseSerializer,
404: OpenApiResponse(description="روش آبیاری یافت نشد"), "روش آبیاری با موفقیت به‌روزرسانی شد.",
),
400: build_response(
IrrigationValidationErrorSerializer,
"داده ورودی نامعتبر است.",
),
404: build_response(
IrrigationValidationErrorSerializer,
"روش آبیاری یافت نشد.",
),
}, },
) )
def patch(self, request, pk): def patch(self, request, pk):
@@ -266,8 +337,14 @@ class IrrigationMethodDetailView(APIView):
summary="حذف روش آبیاری", summary="حذف روش آبیاری",
description="یک روش آبیاری را حذف می‌کند.", description="یک روش آبیاری را حذف می‌کند.",
responses={ responses={
200: OpenApiResponse(description="حذف موفق"), 200: build_response(
404: OpenApiResponse(description="روش آبیاری یافت نشد"), IrrigationValidationErrorSerializer,
"روش آبیاری با موفقیت حذف شد.",
),
404: build_response(
IrrigationValidationErrorSerializer,
"روش آبیاری یافت نشد.",
),
}, },
) )
def delete(self, request, pk): def delete(self, request, pk):
@@ -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/"
}
}
@@ -0,0 +1,5 @@
{
"code": 400,
"msg": "پارامتر sensor_id الزامی است.",
"data": null
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "dashboard-task-123",
"status": "FAILURE",
"error": "خطا در ساخت کارت‌های داشبورد."
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "dashboard-task-123",
"status": "PENDING",
"message": "تسک در صف یا یافت نشد."
}
}
@@ -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"
}
}
}
@@ -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"
}
]
}
}
}
}
}
@@ -0,0 +1,8 @@
{
"code": 202,
"msg": "تسک توصیه کودهی در صف قرار گرفت.",
"data": {
"task_id": "fert-task-123",
"status_url": "/api/fertilization/recommend/fert-task-123/status/"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"sensor_uuid": [
"This field is required."
]
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "fert-task-123",
"status": "FAILURE",
"error": "خطا در دریافت توصیه کودهی."
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "fert-task-123",
"status": "PENDING",
"message": "تسک در صف یا یافت نشد."
}
}
@@ -0,0 +1,11 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "fert-task-123",
"status": "PROGRESS",
"progress": {
"message": "در حال پردازش توصیه کودهی..."
}
}
}
@@ -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"
}
}
}
+604
View File
@@ -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"
}
]
@@ -0,0 +1,5 @@
{
"code": 200,
"msg": "روش آبیاری با موفقیت حذف شد.",
"data": null
}
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "روش آبیاری یافت نشد.",
"data": null
}
@@ -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"
}
}
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "روش آبیاری یافت نشد.",
"data": null
}
@@ -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"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"name": [
"This field may not be blank."
]
}
}
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "روش آبیاری یافت نشد.",
"data": null
}
@@ -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"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"name": [
"This field may not be blank."
]
}
}
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "روش آبیاری یافت نشد.",
"data": null
}
@@ -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"
}
]
}
@@ -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"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"name": [
"This field is required."
]
}
}
@@ -0,0 +1,8 @@
{
"code": 202,
"msg": "تسک توصیه آبیاری در صف قرار گرفت.",
"data": {
"task_id": "irr-task-123",
"status_url": "/api/irrigation/recommend/irr-task-123/status/"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"sensor_uuid": [
"This field is required."
]
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "irr-task-123",
"status": "FAILURE",
"error": "خطا در دریافت توصیه آبیاری."
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "irr-task-123",
"status": "PENDING",
"message": "تسک در صف یا یافت نشد."
}
}
@@ -0,0 +1,11 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "irr-task-123",
"status": "PROGRESS",
"progress": {
"message": "در حال پردازش توصیه آبیاری..."
}
}
}
@@ -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"
}
}
}
+18
View File
@@ -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"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"name": [
"This field is required."
]
}
}
@@ -0,0 +1,5 @@
{
"code": 200,
"msg": "گیاه با موفقیت حذف شد.",
"data": null
}
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "گیاه یافت نشد.",
"data": null
}
+18
View File
@@ -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"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "گیاه یافت نشد.",
"data": null
}
@@ -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"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"name": [
"This field may not be blank."
]
}
}
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "گیاه یافت نشد.",
"data": null
}
+18
View File
@@ -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"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"name": [
"This field may not be blank."
]
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "گیاه یافت نشد.",
"data": null
}
@@ -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"
}
}
@@ -0,0 +1,5 @@
{
"code": 400,
"msg": "نام گیاه الزامی است.",
"data": null
}
@@ -0,0 +1,5 @@
{
"code": 503,
"msg": "سرویس API هنوز پیاده‌سازی نشده است.",
"data": null
}
+20
View File
@@ -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"
}
]
}
@@ -0,0 +1,4 @@
{
"content_type": "text/plain; charset=utf-8",
"body": "سلام، برای بازیابی رطوبت خاک بهتر است آبیاری صبح‌گاهی را تنظیم کنید."
}
@@ -0,0 +1,4 @@
{
"code": 400,
"msg": "service_id نامعتبر است: unknown_service"
}
@@ -0,0 +1,4 @@
{
"code": 400,
"msg": "پارامتر query الزامی است."
}
@@ -0,0 +1,4 @@
{
"code": 400,
"msg": "برای این service_id، پارامتر user_id الزامی است."
}
@@ -0,0 +1,8 @@
{
"code": 202,
"msg": "تسک توصیه کودهی در صف قرار گرفت.",
"data": {
"task_id": "rag-fert-123",
"status_url": "/api/rag/recommend/fertilization/rag-fert-123/status/"
}
}
@@ -0,0 +1,5 @@
{
"code": 400,
"msg": "پارامتر sensor_uuid الزامی است.",
"data": null
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "rag-fert-123",
"status": "FAILURE",
"error": "خطا در دریافت توصیه کودهی."
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "rag-fert-123",
"status": "PENDING",
"message": "تسک در صف یا یافت نشد."
}
}
@@ -0,0 +1,11 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "rag-fert-123",
"status": "PROGRESS",
"progress": {
"message": "در حال پردازش توصیه کودهی..."
}
}
}
@@ -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"
}
}
}
@@ -0,0 +1,8 @@
{
"code": 202,
"msg": "تسک توصیه آبیاری در صف قرار گرفت.",
"data": {
"task_id": "rag-irr-123",
"status_url": "/api/rag/recommend/irrigation/rag-irr-123/status/"
}
}
@@ -0,0 +1,5 @@
{
"code": 400,
"msg": "پارامتر sensor_uuid الزامی است.",
"data": null
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "rag-irr-123",
"status": "FAILURE",
"error": "خطا در دریافت توصیه آبیاری."
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "rag-irr-123",
"status": "PENDING",
"message": "تسک در صف یا یافت نشد."
}
}
@@ -0,0 +1,11 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "rag-irr-123",
"status": "PROGRESS",
"progress": {
"message": "در حال پردازش توصیه آبیاری..."
}
}
}
@@ -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"
}
}
}
@@ -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"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"code": [
"This field is required."
]
}
}
@@ -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"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"location_id": [
"This field is required."
]
}
}
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "location_id یافت نشد.",
"data": null
}
@@ -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"
}
}
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"location_id": [
"This field is required."
]
}
}
@@ -0,0 +1,5 @@
{
"code": 404,
"msg": "location_id یافت نشد.",
"data": null
}
@@ -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
}
]
}
}
@@ -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/"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"lat": [
"This field is required."
],
"lon": [
"This field is required."
]
}
}
@@ -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
}
]
}
}
@@ -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/"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"code": 400,
"msg": "داده نامعتبر.",
"data": {
"lat": [
"A valid number is required."
]
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "soil-task-123",
"status": "FAILURE",
"error": "خطا در واکشی داده خاک."
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "soil-task-123",
"status": "PENDING",
"message": "تسک در صف یا یافت نشد."
}
}
@@ -0,0 +1,12 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "soil-task-123",
"status": "PROGRESS",
"progress": {
"step": "fetch",
"percent": 60
}
}
}
@@ -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
}
]
}
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "sample-task-123"
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "sample-task-123",
"status": "FAILURE",
"error": "Sample task failed."
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "sample-task-123",
"status": "PENDING",
"message": "تسک در صف یا یافت نشد."
}
}
@@ -0,0 +1,13 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "sample-task-123",
"status": "PROGRESS",
"progress": {
"current": 1,
"total": 3,
"message": "در حال پردازش..."
}
}
}
@@ -0,0 +1,9 @@
{
"code": 200,
"msg": "success",
"data": {
"task_id": "sample-task-123",
"status": "SUCCESS",
"result": "done"
}
}
+1 -1
View File
@@ -73,4 +73,4 @@ class SoilDataTaskResponseSerializer(serializers.Serializer):
task_id = serializers.CharField() task_id = serializers.CharField()
lon = serializers.FloatField(source="longitude") lon = serializers.FloatField(source="longitude")
lat = serializers.FloatField(source="latitude") lat = serializers.FloatField(source="latitude")
status_url = serializers.URLField(required=False) status_url = serializers.CharField(required=False)
+62 -23
View File
@@ -9,15 +9,50 @@ from rest_framework import serializers as drf_serializers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView 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 .models import SoilLocation
from .serializers import ( from .serializers import (
SoilDataRequestSerializer, SoilDataRequestSerializer,
SoilDepthDataSerializer,
SoilDataTaskResponseSerializer, SoilDataTaskResponseSerializer,
SoilLocationResponseSerializer, SoilLocationResponseSerializer,
) )
from .tasks import fetch_soil_data_task 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): class SoilDataView(APIView):
""" """
API خاک: مختصات جغرافیایی را میگیرد. API خاک: مختصات جغرافیایی را میگیرد.
@@ -49,9 +84,18 @@ class SoilDataView(APIView):
}, },
], ],
responses={ responses={
200: OpenApiResponse(description="داده خاک از دیتابیس"), 200: build_response(
202: OpenApiResponse(description="تسک در صف قرار گرفت"), SoilDataResponseSerializer,
400: OpenApiResponse(description="داده نامعتبر"), "داده خاک از دیتابیس بازگردانده شد.",
),
202: build_response(
SoilTaskQueuedResponseSerializer,
"تسک واکشی داده خاک در صف قرار گرفت.",
),
400: build_response(
SoilErrorResponseSerializer,
"پارامترهای ورودی نامعتبر هستند.",
),
}, },
) )
def get(self, request): def get(self, request):
@@ -63,9 +107,18 @@ class SoilDataView(APIView):
description="با ارسال lat و lon در بدنه، داده خاک از DB یا از طریق تسک Celery برگردانده می‌شود.", description="با ارسال lat و lon در بدنه، داده خاک از DB یا از طریق تسک Celery برگردانده می‌شود.",
request=SoilDataRequestSerializer, request=SoilDataRequestSerializer,
responses={ responses={
200: OpenApiResponse(description="داده خاک از دیتابیس"), 200: build_response(
202: OpenApiResponse(description="تسک در صف قرار گرفت"), SoilDataResponseSerializer,
400: OpenApiResponse(description="داده نامعتبر"), "داده خاک از دیتابیس بازگردانده شد.",
),
202: build_response(
SoilTaskQueuedResponseSerializer,
"تسک واکشی داده خاک در صف قرار گرفت.",
),
400: build_response(
SoilErrorResponseSerializer,
"پارامترهای ورودی نامعتبر هستند.",
),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -141,23 +194,9 @@ class SoilDataTaskStatusView(APIView):
summary="وضعیت تسک داده خاک", summary="وضعیت تسک داده خاک",
description="وضعیت تسک Celery واکشی داده خاک را برمی‌گرداند.", description="وضعیت تسک Celery واکشی داده خاک را برمی‌گرداند.",
responses={ responses={
200: inline_serializer( 200: build_response(
name="SoilTaskStatusResponse", SoilTaskStatusResponseSerializer,
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),
},
),
},
), ),
}, },
) )
+87 -16
View File
@@ -9,11 +9,32 @@ from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from config.openapi import build_envelope_serializer, build_response
from .models import Plant from .models import Plant
from .serializers import PlantSerializer from .serializers import PlantSerializer
from .services import fetch_plant_info_from_api 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): class PlantListCreateView(APIView):
"""لیست تمام گیاهان و ایجاد گیاه جدید.""" """لیست تمام گیاهان و ایجاد گیاه جدید."""
@@ -21,7 +42,12 @@ class PlantListCreateView(APIView):
tags=["Plant"], tags=["Plant"],
summary="لیست گیاهان", summary="لیست گیاهان",
description="لیست تمام گیاهان ذخیره‌شده را برمی‌گرداند.", description="لیست تمام گیاهان ذخیره‌شده را برمی‌گرداند.",
responses={200: PlantSerializer(many=True)}, responses={
200: build_response(
PlantListResponseSerializer,
"لیست گیاهان ذخیره‌شده.",
),
},
) )
def get(self, request): def get(self, request):
plants = Plant.objects.all() plants = Plant.objects.all()
@@ -37,8 +63,14 @@ class PlantListCreateView(APIView):
description="یک گیاه جدید با مشخصات داده‌شده ایجاد می‌کند.", description="یک گیاه جدید با مشخصات داده‌شده ایجاد می‌کند.",
request=PlantSerializer, request=PlantSerializer,
responses={ responses={
201: PlantSerializer, 201: build_response(
400: OpenApiResponse(description="داده نامعتبر"), PlantDetailResponseSerializer,
"گیاه جدید با موفقیت ایجاد شد.",
),
400: build_response(
PlantValidationErrorSerializer,
"داده ورودی نامعتبر است.",
),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -83,8 +115,14 @@ class PlantDetailView(APIView):
summary="جزئیات گیاه", summary="جزئیات گیاه",
description="مشخصات یک گیاه را بر اساس شناسه برمی‌گرداند.", description="مشخصات یک گیاه را بر اساس شناسه برمی‌گرداند.",
responses={ responses={
200: PlantSerializer, 200: build_response(
404: OpenApiResponse(description="گیاه یافت نشد"), PlantDetailResponseSerializer,
"جزئیات گیاه.",
),
404: build_response(
PlantValidationErrorSerializer,
"گیاه یافت نشد.",
),
}, },
) )
def get(self, request, pk): def get(self, request, pk):
@@ -106,9 +144,18 @@ class PlantDetailView(APIView):
description="تمام فیلدهای یک گیاه را آپدیت می‌کند.", description="تمام فیلدهای یک گیاه را آپدیت می‌کند.",
request=PlantSerializer, request=PlantSerializer,
responses={ responses={
200: PlantSerializer, 200: build_response(
400: OpenApiResponse(description="داده نامعتبر"), PlantDetailResponseSerializer,
404: OpenApiResponse(description="گیاه یافت نشد"), "گیاه با موفقیت به‌روزرسانی شد.",
),
400: build_response(
PlantValidationErrorSerializer,
"داده ورودی نامعتبر است.",
),
404: build_response(
PlantValidationErrorSerializer,
"گیاه یافت نشد.",
),
}, },
) )
def put(self, request, pk): def put(self, request, pk):
@@ -136,9 +183,18 @@ class PlantDetailView(APIView):
description="فقط فیلدهای ارسال‌شده آپدیت می‌شوند.", description="فقط فیلدهای ارسال‌شده آپدیت می‌شوند.",
request=PlantSerializer, request=PlantSerializer,
responses={ responses={
200: PlantSerializer, 200: build_response(
400: OpenApiResponse(description="داده نامعتبر"), PlantDetailResponseSerializer,
404: OpenApiResponse(description="گیاه یافت نشد"), "گیاه با موفقیت به‌روزرسانی شد.",
),
400: build_response(
PlantValidationErrorSerializer,
"داده ورودی نامعتبر است.",
),
404: build_response(
PlantValidationErrorSerializer,
"گیاه یافت نشد.",
),
}, },
) )
def patch(self, request, pk): def patch(self, request, pk):
@@ -165,8 +221,14 @@ class PlantDetailView(APIView):
summary="حذف گیاه", summary="حذف گیاه",
description="یک گیاه را حذف می‌کند.", description="یک گیاه را حذف می‌کند.",
responses={ responses={
200: OpenApiResponse(description="حذف موفق"), 200: build_response(
404: OpenApiResponse(description="گیاه یافت نشد"), PlantValidationErrorSerializer,
"گیاه با موفقیت حذف شد.",
),
404: build_response(
PlantValidationErrorSerializer,
"گیاه یافت نشد.",
),
}, },
) )
def delete(self, request, pk): def delete(self, request, pk):
@@ -197,9 +259,18 @@ class PlantFetchInfoView(APIView):
}, },
), ),
responses={ responses={
200: PlantSerializer, 200: build_response(
400: OpenApiResponse(description="نام گیاه ارسال نشده"), PlantFetchInfoResponseSerializer,
503: OpenApiResponse(description="سرویس در دسترس نیست"), "اطلاعات گیاه از سرویس خارجی دریافت شد.",
),
400: build_response(
PlantValidationErrorSerializer,
"نام گیاه ارسال نشده است.",
),
503: build_response(
PlantValidationErrorSerializer,
"سرویس خارجی در دسترس نیست.",
),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
+58 -61
View File
@@ -1,7 +1,10 @@
""" """
ویوهای RAG چت با استریم ویوهای RAG چت با استریم
""" """
import logging
from django.http import StreamingHttpResponse from django.http import StreamingHttpResponse
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import ( from drf_spectacular.utils import (
OpenApiExample, OpenApiExample,
OpenApiResponse, OpenApiResponse,
@@ -13,14 +16,46 @@ from rest_framework import serializers as drf_serializers
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView 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 from .chat import chat_rag_stream
logger = logging.getLogger(__name__) 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): class ChatView(APIView):
""" """
چت RAG با استریم. چت RAG با استریم.
@@ -44,10 +79,12 @@ class ChatView(APIView):
), ),
responses={ responses={
200: OpenApiResponse( 200: OpenApiResponse(
response=OpenApiTypes.STR,
description="پاسخ استریم متنی (text/plain)", description="پاسخ استریم متنی (text/plain)",
), ),
400: OpenApiResponse( 400: build_response(
description="پارامتر ورودی نامعتبر", RagChatErrorResponseSerializer,
"پارامترهای ورودی نامعتبر هستند.",
), ),
}, },
examples=[ examples=[
@@ -152,21 +189,14 @@ class IrrigationRecommendationView(APIView):
}, },
), ),
responses={ responses={
202: inline_serializer( 202: build_response(
name="IrrigationRecommendationResponse", RagIrrigationQueueResponseSerializer,
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(),
},
), ),
}, 400: build_response(
RagValidationErrorResponseSerializer,
"پارامتر ورودی نامعتبر است.",
), ),
400: OpenApiResponse(description="پارامتر ورودی نامعتبر"),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -219,22 +249,9 @@ class IrrigationRecommendationStatusView(APIView):
summary="وضعیت تسک توصیه آبیاری", summary="وضعیت تسک توصیه آبیاری",
description="وضعیت تسک Celery توصیه آبیاری را برمی‌گرداند.", description="وضعیت تسک Celery توصیه آبیاری را برمی‌گرداند.",
responses={ responses={
200: inline_serializer( 200: build_response(
name="IrrigationRecommendationStatusResponse", RagIrrigationStatusResponseSerializer,
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),
},
),
},
), ),
}, },
) )
@@ -281,21 +298,14 @@ class FertilizationRecommendationView(APIView):
}, },
), ),
responses={ responses={
202: inline_serializer( 202: build_response(
name="FertilizationRecommendationResponse", RagFertilizationQueueResponseSerializer,
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(),
},
), ),
}, 400: build_response(
RagValidationErrorResponseSerializer,
"پارامتر ورودی نامعتبر است.",
), ),
400: OpenApiResponse(description="پارامتر ورودی نامعتبر"),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -346,22 +356,9 @@ class FertilizationRecommendationStatusView(APIView):
summary="وضعیت تسک توصیه کودهی", summary="وضعیت تسک توصیه کودهی",
description="وضعیت تسک Celery توصیه کودهی را برمی‌گرداند.", description="وضعیت تسک Celery توصیه کودهی را برمی‌گرداند.",
responses={ responses={
200: inline_serializer( 200: build_response(
name="FertilizationRecommendationStatusResponse", RagFertilizationStatusResponseSerializer,
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),
},
),
},
), ),
}, },
) )
+829
View File
@@ -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()
+61 -23
View File
@@ -10,6 +10,7 @@ from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from config.openapi import build_envelope_serializer, build_response
from location_data.models import SoilLocation from location_data.models import SoilLocation
from .models import ParameterUpdateLog, SensorData, SensorDataHistory, SensorParameter 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): class SensorDataUpdateView(APIView):
""" """
آپدیت داده سنسور. هنگام آپدیت، نسخه فعلی در SensorDataHistory ذخیره میشود. آپدیت داده سنسور. هنگام آپدیت، نسخه فعلی در SensorDataHistory ذخیره میشود.
@@ -31,9 +62,18 @@ class SensorDataUpdateView(APIView):
description="داده سنسور را بر اساس uuid_sensor آپدیت (یا ایجاد) می‌کند. نسخه قبلی در تاریخچه ذخیره می‌شود.", description="داده سنسور را بر اساس uuid_sensor آپدیت (یا ایجاد) می‌کند. نسخه قبلی در تاریخچه ذخیره می‌شود.",
request=SensorDataUpdateSerializer, request=SensorDataUpdateSerializer,
responses={ responses={
200: SensorDataResponseSerializer, 200: build_response(
400: OpenApiResponse(description="داده نامعتبر"), SensorDataEnvelopeSerializer,
404: OpenApiResponse(description="location_id یافت نشد"), "داده سنسور با موفقیت ایجاد یا به‌روزرسانی شد.",
),
400: build_response(
SensorDataValidationErrorSerializer,
"داده ورودی نامعتبر است.",
),
404: build_response(
SensorDataNotFoundSerializer,
"location_id یافت نشد.",
),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -61,9 +101,18 @@ class SensorDataUpdateView(APIView):
description="فقط فیلدهای ارسال‌شده آپدیت می‌شوند.", description="فقط فیلدهای ارسال‌شده آپدیت می‌شوند.",
request=SensorDataUpdateSerializer, request=SensorDataUpdateSerializer,
responses={ responses={
200: SensorDataResponseSerializer, 200: build_response(
400: OpenApiResponse(description="داده نامعتبر"), SensorDataEnvelopeSerializer,
404: OpenApiResponse(description="location_id یافت نشد"), "داده سنسور با موفقیت به‌روزرسانی شد.",
),
400: build_response(
SensorDataValidationErrorSerializer,
"داده ورودی نامعتبر است.",
),
404: build_response(
SensorDataNotFoundSerializer,
"location_id یافت نشد.",
),
}, },
) )
def patch(self, request, uuid_sensor): def patch(self, request, uuid_sensor):
@@ -137,25 +186,14 @@ class SensorParameterCreateView(APIView):
description="پارامتر جدید اضافه یا پارامتر موجود را ویرایش می‌کند و در لاگ ثبت می‌شود.", description="پارامتر جدید اضافه یا پارامتر موجود را ویرایش می‌کند و در لاگ ثبت می‌شود.",
request=SensorParameterSerializer, request=SensorParameterSerializer,
responses={ responses={
201: inline_serializer( 201: build_response(
name="SensorParameterResponse", SensorParameterResponseSerializer,
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(),
},
), ),
}, 400: build_response(
SensorDataValidationErrorSerializer,
"داده ورودی نامعتبر است.",
), ),
400: OpenApiResponse(description="داده نامعتبر"),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
+27 -29
View File
@@ -11,9 +11,30 @@ from rest_framework import serializers as drf_serializers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView 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 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): class TaskTriggerView(APIView):
""" """
ثبت و اجرای تسک. ثبت و اجرای تسک.
@@ -33,18 +54,9 @@ class TaskTriggerView(APIView):
}, },
), ),
responses={ responses={
200: inline_serializer( 200: build_response(
name="TaskTriggerResponse", TaskTriggerResponseSerializer,
fields={ "تسک نمونه با موفقیت در صف قرار گرفت.",
"code": drf_serializers.IntegerField(),
"msg": drf_serializers.CharField(),
"data": inline_serializer(
name="TaskTriggerData",
fields={
"task_id": drf_serializers.CharField(),
},
),
},
), ),
}, },
examples=[ examples=[
@@ -85,23 +97,9 @@ class TaskStatusView(APIView):
summary="وضعیت تسک", summary="وضعیت تسک",
description="وضعیت یک تسک Celery را بر اساس task_id برمی‌گرداند.", description="وضعیت یک تسک Celery را بر اساس task_id برمی‌گرداند.",
responses={ responses={
200: inline_serializer( 200: build_response(
name="TaskStatusResponse", TaskStatusResponseSerializer,
fields={ "وضعیت فعلی تسک Celery.",
"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),
},
),
},
), ),
}, },
examples=[ examples=[