UPDATE
This commit is contained in:
@@ -0,0 +1,282 @@
|
|||||||
|
# Crop Zoning Layer Area API Changes For Frontend
|
||||||
|
|
||||||
|
این فایل برای تیم فرانت نوشته شده و فقط تغییرات جدیدی را توضیح میدهد که برای endpointهای لایهای ماژول `crop-zoning` اضافه شدهاند.
|
||||||
|
|
||||||
|
## خلاصه تغییر
|
||||||
|
|
||||||
|
سه API جدید اضافه شدهاند که از نظر ساختار response دقیقا شبیه `GET /area/` هستند:
|
||||||
|
|
||||||
|
- `GET /api/crop-zoning/water-need/`
|
||||||
|
- `GET /api/crop-zoning/soil-quality/`
|
||||||
|
- `GET /api/crop-zoning/cultivation-risk/`
|
||||||
|
|
||||||
|
هر سه endpoint:
|
||||||
|
|
||||||
|
- به `farm_uuid` نیاز دارند
|
||||||
|
- از `page` و `page_size` پشتیبانی میکنند
|
||||||
|
- در صورت نبود داده، همان روند ساخت area و zone و dispatch task را مثل `area/` انجام میدهند
|
||||||
|
- همان ساختار `task`, `area`, `zones`, `pagination` را برمیگردانند
|
||||||
|
|
||||||
|
## هدف این تغییر
|
||||||
|
|
||||||
|
قبلا فرانت برای دادههای لایهای بیشتر به endpointهای `zones/...` متکی بود که خروجی آنها فقط لیست زونها بود.
|
||||||
|
الان برای هر لایه یک endpoint جدید دارید که خروجی آن برای صفحه map دقیقا با `area/` همفرمت است و استفاده در UI سادهتر میشود.
|
||||||
|
|
||||||
|
## Base Path
|
||||||
|
|
||||||
|
```text
|
||||||
|
/api/crop-zoning/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpointهای جدید
|
||||||
|
|
||||||
|
### 1) Water Need
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/crop-zoning/water-need/?farm_uuid=<farm_uuid>&page=1&page_size=10
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Soil Quality
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/crop-zoning/soil-quality/?farm_uuid=<farm_uuid>&page=1&page_size=10
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3) Cultivation Risk
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/crop-zoning/cultivation-risk/?farm_uuid=<farm_uuid>&page=1&page_size=10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Params
|
||||||
|
|
||||||
|
- `farm_uuid`: اجباری، UUID مزرعه
|
||||||
|
- `page`: اختیاری، شماره صفحه زونها، پیشفرض `1`
|
||||||
|
- `page_size`: اختیاری، تعداد زون در هر صفحه، پیشفرض `10`
|
||||||
|
|
||||||
|
## ساختار کلی response
|
||||||
|
|
||||||
|
ساختار کلی هر سه API:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"task": {},
|
||||||
|
"area": {},
|
||||||
|
"zones": [],
|
||||||
|
"pagination": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
یعنی برای فرانت:
|
||||||
|
|
||||||
|
- `task` دقیقا مثل `area/` است
|
||||||
|
- `area` دقیقا مثل `area/` است
|
||||||
|
- `pagination` دقیقا مثل `area/` است
|
||||||
|
- فقط shape آیتمهای داخل `zones` بر اساس نوع لایه فرق میکند
|
||||||
|
|
||||||
|
## تفاوت `zones` در هر endpoint
|
||||||
|
|
||||||
|
### `GET /water-need/`
|
||||||
|
|
||||||
|
هر آیتم در `zones` این فیلدها را دارد:
|
||||||
|
|
||||||
|
- `zoneId`
|
||||||
|
- `zoneUuid`
|
||||||
|
- `geometry`
|
||||||
|
- `center`
|
||||||
|
- `area_sqm`
|
||||||
|
- `area_hectares`
|
||||||
|
- `sequence`
|
||||||
|
- `processing_status`
|
||||||
|
- `processing_error`
|
||||||
|
- `waterNeedLayer`
|
||||||
|
|
||||||
|
نمونه:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"zoneId": "zone-0",
|
||||||
|
"zoneUuid": "d7a9a19b-b3ec-4721-b514-9aae5c9ea940",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[[51.384258, 35.689389], [51.38536404, 35.689389], [51.38536404, 35.69028731], [51.384258, 35.69028731], [51.384258, 35.689389]]]
|
||||||
|
},
|
||||||
|
"center": {
|
||||||
|
"latitude": 35.68983816,
|
||||||
|
"longitude": 51.38481102
|
||||||
|
},
|
||||||
|
"area_sqm": 9999.91,
|
||||||
|
"area_hectares": 1,
|
||||||
|
"sequence": 0,
|
||||||
|
"processing_status": "completed",
|
||||||
|
"processing_error": "",
|
||||||
|
"waterNeedLayer": {
|
||||||
|
"level": "medium",
|
||||||
|
"value": "4820-5820 m³/ha",
|
||||||
|
"color": "#0ea5e9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /soil-quality/`
|
||||||
|
|
||||||
|
هر آیتم در `zones` این فیلدها را دارد:
|
||||||
|
|
||||||
|
- `zoneId`
|
||||||
|
- `zoneUuid`
|
||||||
|
- `geometry`
|
||||||
|
- `center`
|
||||||
|
- `area_sqm`
|
||||||
|
- `area_hectares`
|
||||||
|
- `sequence`
|
||||||
|
- `processing_status`
|
||||||
|
- `processing_error`
|
||||||
|
- `soilQualityLayer`
|
||||||
|
|
||||||
|
نمونه:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"zoneId": "zone-0",
|
||||||
|
"zoneUuid": "d7a9a19b-b3ec-4721-b514-9aae5c9ea940",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[[51.384258, 35.689389], [51.38536404, 35.689389], [51.38536404, 35.69028731], [51.384258, 35.69028731], [51.384258, 35.689389]]]
|
||||||
|
},
|
||||||
|
"center": {
|
||||||
|
"latitude": 35.68983816,
|
||||||
|
"longitude": 51.38481102
|
||||||
|
},
|
||||||
|
"area_sqm": 9999.91,
|
||||||
|
"area_hectares": 1,
|
||||||
|
"sequence": 0,
|
||||||
|
"processing_status": "completed",
|
||||||
|
"processing_error": "",
|
||||||
|
"soilQualityLayer": {
|
||||||
|
"level": "high",
|
||||||
|
"score": 89,
|
||||||
|
"color": "#22c55e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /cultivation-risk/`
|
||||||
|
|
||||||
|
هر آیتم در `zones` این فیلدها را دارد:
|
||||||
|
|
||||||
|
- `zoneId`
|
||||||
|
- `zoneUuid`
|
||||||
|
- `geometry`
|
||||||
|
- `center`
|
||||||
|
- `area_sqm`
|
||||||
|
- `area_hectares`
|
||||||
|
- `sequence`
|
||||||
|
- `processing_status`
|
||||||
|
- `processing_error`
|
||||||
|
- `cultivationRiskLayer`
|
||||||
|
|
||||||
|
نمونه:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"zoneId": "zone-0",
|
||||||
|
"zoneUuid": "d7a9a19b-b3ec-4721-b514-9aae5c9ea940",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[[51.384258, 35.689389], [51.38536404, 35.689389], [51.38536404, 35.69028731], [51.384258, 35.69028731], [51.384258, 35.689389]]]
|
||||||
|
},
|
||||||
|
"center": {
|
||||||
|
"latitude": 35.68983816,
|
||||||
|
"longitude": 51.38481102
|
||||||
|
},
|
||||||
|
"area_sqm": 9999.91,
|
||||||
|
"area_hectares": 1,
|
||||||
|
"sequence": 0,
|
||||||
|
"processing_status": "completed",
|
||||||
|
"processing_error": "",
|
||||||
|
"cultivationRiskLayer": {
|
||||||
|
"level": "low",
|
||||||
|
"color": "#22c55e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## نکته مهم برای فرانت
|
||||||
|
|
||||||
|
این endpointها عمدا شبیه `area/` طراحی شدهاند تا فرانت بتواند با یک data flow یکسان کار کند:
|
||||||
|
|
||||||
|
- polygon اصلی را از `data.area` بگیرد
|
||||||
|
- task status را از `data.task` بخواند
|
||||||
|
- pagination را از `data.pagination` بخواند
|
||||||
|
- فقط renderer مربوط به هر لایه را روی `data.zones` اعمال کند
|
||||||
|
|
||||||
|
## پیشنهاد استفاده در UI
|
||||||
|
|
||||||
|
### اگر صفحه overview اصلی دارید
|
||||||
|
|
||||||
|
- همچنان `GET /area/` بهترین گزینه برای صفحه overview کامل است، چون علاوه بر layerها فیلدهای crop و recommendation را هم داخل هر zone دارد.
|
||||||
|
|
||||||
|
### اگر صفحه یا tab مخصوص هر layer دارید
|
||||||
|
|
||||||
|
- برای تب نیاز آبی: `GET /water-need/`
|
||||||
|
- برای تب کیفیت خاک: `GET /soil-quality/`
|
||||||
|
- برای تب ریسک کشت: `GET /cultivation-risk/`
|
||||||
|
|
||||||
|
این کار باعث میشود فرانت فقط داده موردنیاز همان layer را بگیرد.
|
||||||
|
|
||||||
|
## وضعیت backward compatibility
|
||||||
|
|
||||||
|
- endpoint قدیمی `GET /area/` بدون تغییر باقی مانده است
|
||||||
|
- endpointهای جدید breaking change ایجاد نمیکنند
|
||||||
|
- فقط سه مسیر جدید به API اضافه شده است
|
||||||
|
|
||||||
|
## خطاها
|
||||||
|
|
||||||
|
رفتار خطاها مثل `area/` است.
|
||||||
|
|
||||||
|
### نبودن `farm_uuid`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "farm_uuid is required."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### پیدا نشدن مزرعه
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Farm not found."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### نامعتبر بودن `page` یا `page_size`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "page must be a positive integer."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## جمعبندی
|
||||||
|
|
||||||
|
تغییر جدید برای فرانت این است که الان به جز `area/`، سه API جدید هم دارید که:
|
||||||
|
|
||||||
|
- از نظر query params شبیه `area/` هستند
|
||||||
|
- از نظر response wrapper شبیه `area/` هستند
|
||||||
|
- فقط payload داخلی `zones` را بر اساس نوع layer تخصصی میکنند
|
||||||
|
|
||||||
|
در نتیجه اگر UI شما برای `area/` آماده است، اتصال این سه endpoint جدید باید با کمترین تغییر انجام شود.
|
||||||
+114
-37
@@ -514,20 +514,13 @@ def build_initial_zone_payload(zone):
|
|||||||
|
|
||||||
|
|
||||||
def build_area_zone_payload(zone):
|
def build_area_zone_payload(zone):
|
||||||
|
base_payload = _build_area_layer_zone_base_payload(zone)
|
||||||
recommendation = getattr(zone, "recommendation", None)
|
recommendation = getattr(zone, "recommendation", None)
|
||||||
water_need_layer = getattr(zone, "water_need_layer", None)
|
water_need_layer = getattr(zone, "water_need_layer", None)
|
||||||
soil_quality_layer = getattr(zone, "soil_quality_layer", None)
|
soil_quality_layer = getattr(zone, "soil_quality_layer", None)
|
||||||
cultivation_risk_layer = getattr(zone, "cultivation_risk_layer", None)
|
cultivation_risk_layer = getattr(zone, "cultivation_risk_layer", None)
|
||||||
return {
|
base_payload.update(
|
||||||
"zoneId": zone.zone_id,
|
{
|
||||||
"zoneUuid": str(zone.uuid),
|
|
||||||
"geometry": zone.geometry,
|
|
||||||
"center": zone.center,
|
|
||||||
"area_sqm": zone.area_sqm,
|
|
||||||
"area_hectares": zone.area_hectares,
|
|
||||||
"sequence": zone.sequence,
|
|
||||||
"processing_status": zone.processing_status,
|
|
||||||
"processing_error": zone.processing_error,
|
|
||||||
"crop": recommendation.product.product_id if recommendation else "",
|
"crop": recommendation.product.product_id if recommendation else "",
|
||||||
"matchPercent": recommendation.match_percent if recommendation else 0,
|
"matchPercent": recommendation.match_percent if recommendation else 0,
|
||||||
"waterNeed": recommendation.water_need if recommendation else "",
|
"waterNeed": recommendation.water_need if recommendation else "",
|
||||||
@@ -547,6 +540,54 @@ def build_area_zone_payload(zone):
|
|||||||
"color": getattr(cultivation_risk_layer, "color", ""),
|
"color": getattr(cultivation_risk_layer, "color", ""),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
return base_payload
|
||||||
|
|
||||||
|
|
||||||
|
def _build_area_layer_zone_base_payload(zone):
|
||||||
|
return {
|
||||||
|
"zoneId": zone.zone_id,
|
||||||
|
"zoneUuid": str(zone.uuid),
|
||||||
|
"geometry": zone.geometry,
|
||||||
|
"center": zone.center,
|
||||||
|
"area_sqm": zone.area_sqm,
|
||||||
|
"area_hectares": zone.area_hectares,
|
||||||
|
"sequence": zone.sequence,
|
||||||
|
"processing_status": zone.processing_status,
|
||||||
|
"processing_error": zone.processing_error,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_water_need_area_zone_payload(zone):
|
||||||
|
base_payload = _build_area_layer_zone_base_payload(zone)
|
||||||
|
water_need_layer = getattr(zone, "water_need_layer", None)
|
||||||
|
base_payload["waterNeedLayer"] = {
|
||||||
|
"level": getattr(water_need_layer, "level", ""),
|
||||||
|
"value": getattr(water_need_layer, "value", ""),
|
||||||
|
"color": getattr(water_need_layer, "color", ""),
|
||||||
|
}
|
||||||
|
return base_payload
|
||||||
|
|
||||||
|
|
||||||
|
def build_soil_quality_area_zone_payload(zone):
|
||||||
|
base_payload = _build_area_layer_zone_base_payload(zone)
|
||||||
|
soil_quality_layer = getattr(zone, "soil_quality_layer", None)
|
||||||
|
base_payload["soilQualityLayer"] = {
|
||||||
|
"level": getattr(soil_quality_layer, "level", ""),
|
||||||
|
"score": getattr(soil_quality_layer, "score", 0),
|
||||||
|
"color": getattr(soil_quality_layer, "color", ""),
|
||||||
|
}
|
||||||
|
return base_payload
|
||||||
|
|
||||||
|
|
||||||
|
def build_cultivation_risk_area_zone_payload(zone):
|
||||||
|
base_payload = _build_area_layer_zone_base_payload(zone)
|
||||||
|
cultivation_risk_layer = getattr(zone, "cultivation_risk_layer", None)
|
||||||
|
base_payload["cultivationRiskLayer"] = {
|
||||||
|
"level": getattr(cultivation_risk_layer, "level", ""),
|
||||||
|
"color": getattr(cultivation_risk_layer, "color", ""),
|
||||||
|
}
|
||||||
|
return base_payload
|
||||||
|
|
||||||
|
|
||||||
def persist_zone_analysis_metrics(zone, metrics):
|
def persist_zone_analysis_metrics(zone, metrics):
|
||||||
@@ -949,9 +990,38 @@ def _zones_queryset(zone_ids=None):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
def get_latest_area_payload(area=None, page=1, page_size=DEFAULT_ZONE_PAGE_SIZE):
|
def _get_idle_area_payload(page, page_size):
|
||||||
|
return {
|
||||||
|
"task": {
|
||||||
|
"status": "IDLE",
|
||||||
|
"area_uuid": "",
|
||||||
|
"total_zones": 0,
|
||||||
|
"completed_zones": 0,
|
||||||
|
"processing_zones": 0,
|
||||||
|
"pending_zones": 0,
|
||||||
|
"failed_zones": 0,
|
||||||
|
"failed_zone_errors": [],
|
||||||
|
"cell_side_km": round(get_default_cell_side_km(), 4),
|
||||||
|
},
|
||||||
|
"area": get_default_area_feature(),
|
||||||
|
"zones": [],
|
||||||
|
"pagination": {
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"total_pages": 0,
|
||||||
|
"total_zones": 0,
|
||||||
|
"returned_zones": 0,
|
||||||
|
"has_next": False,
|
||||||
|
"has_previous": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_latest_area_layer_payload(zone_builder, area=None, page=1, page_size=DEFAULT_ZONE_PAGE_SIZE):
|
||||||
area = area or CropArea.objects.order_by("-created_at", "-id").first()
|
area = area or CropArea.objects.order_by("-created_at", "-id").first()
|
||||||
if area:
|
if not area:
|
||||||
|
return _get_idle_area_payload(page, page_size)
|
||||||
|
|
||||||
status_zones = list(area.zones.only("zone_id", "task_id", "processing_status", "processing_error"))
|
status_zones = list(area.zones.only("zone_id", "task_id", "processing_status", "processing_error"))
|
||||||
total_zones = len(status_zones)
|
total_zones = len(status_zones)
|
||||||
completed_zones = sum(1 for zone in status_zones if zone.processing_status == CropZone.STATUS_COMPLETED)
|
completed_zones = sum(1 for zone in status_zones if zone.processing_status == CropZone.STATUS_COMPLETED)
|
||||||
@@ -1026,7 +1096,7 @@ def get_latest_area_payload(area=None, page=1, page_size=DEFAULT_ZONE_PAGE_SIZE)
|
|||||||
"cell_side_km": round(math.sqrt(max(area.chunk_area_sqm, 1)) / 1000.0, 4),
|
"cell_side_km": round(math.sqrt(max(area.chunk_area_sqm, 1)) / 1000.0, 4),
|
||||||
},
|
},
|
||||||
"area": area.geometry,
|
"area": area.geometry,
|
||||||
"zones": [build_area_zone_payload(zone) for zone in zones],
|
"zones": [zone_builder(zone) for zone in zones],
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
@@ -1037,30 +1107,37 @@ def get_latest_area_payload(area=None, page=1, page_size=DEFAULT_ZONE_PAGE_SIZE)
|
|||||||
"has_previous": page > 1 and total_pages > 0,
|
"has_previous": page > 1 and total_pages > 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
"task": {
|
|
||||||
"status": "IDLE",
|
def get_latest_area_payload(area=None, page=1, page_size=DEFAULT_ZONE_PAGE_SIZE):
|
||||||
"area_uuid": "",
|
return _build_latest_area_layer_payload(build_area_zone_payload, area=area, page=page, page_size=page_size)
|
||||||
"total_zones": 0,
|
|
||||||
"completed_zones": 0,
|
|
||||||
"processing_zones": 0,
|
def get_latest_water_need_payload(area=None, page=1, page_size=DEFAULT_ZONE_PAGE_SIZE):
|
||||||
"pending_zones": 0,
|
return _build_latest_area_layer_payload(
|
||||||
"failed_zones": 0,
|
build_water_need_area_zone_payload,
|
||||||
"failed_zone_errors": [],
|
area=area,
|
||||||
"cell_side_km": round(get_default_cell_side_km(), 4),
|
page=page,
|
||||||
},
|
page_size=page_size,
|
||||||
"area": get_default_area_feature(),
|
)
|
||||||
"zones": [],
|
|
||||||
"pagination": {
|
|
||||||
"page": page,
|
def get_latest_soil_quality_payload(area=None, page=1, page_size=DEFAULT_ZONE_PAGE_SIZE):
|
||||||
"page_size": page_size,
|
return _build_latest_area_layer_payload(
|
||||||
"total_pages": 0,
|
build_soil_quality_area_zone_payload,
|
||||||
"total_zones": 0,
|
area=area,
|
||||||
"returned_zones": 0,
|
page=page,
|
||||||
"has_next": False,
|
page_size=page_size,
|
||||||
"has_previous": False,
|
)
|
||||||
},
|
|
||||||
}
|
|
||||||
|
def get_latest_cultivation_risk_payload(area=None, page=1, page_size=DEFAULT_ZONE_PAGE_SIZE):
|
||||||
|
return _build_latest_area_layer_payload(
|
||||||
|
build_cultivation_risk_area_zone_payload,
|
||||||
|
area=area,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_initial_zones_payload(crop_area):
|
def get_initial_zones_payload(crop_area):
|
||||||
|
|||||||
+105
-1
@@ -8,7 +8,13 @@ from kombu.exceptions import OperationalError
|
|||||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||||
|
|
||||||
from crop_zoning.models import CropArea, CropZone
|
from crop_zoning.models import CropArea, CropZone
|
||||||
from crop_zoning.views import AreaView, ZonesInitialView
|
from crop_zoning.views import (
|
||||||
|
AreaView,
|
||||||
|
CultivationRiskView,
|
||||||
|
SoilQualityView,
|
||||||
|
WaterNeedView,
|
||||||
|
ZonesInitialView,
|
||||||
|
)
|
||||||
from farm_hub.models import FarmHub, FarmType
|
from farm_hub.models import FarmHub, FarmType
|
||||||
|
|
||||||
|
|
||||||
@@ -313,3 +319,101 @@ class AreaViewTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
mock_dispatch.assert_called_once_with(zone_ids=[stale_zone.id], force=True)
|
mock_dispatch.assert_called_once_with(zone_ids=[stale_zone.id], force=True)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
USE_EXTERNAL_API_MOCK=True,
|
||||||
|
CROP_ZONE_CHUNK_AREA_SQM=200000,
|
||||||
|
)
|
||||||
|
class LayerAreaViewTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = APIRequestFactory()
|
||||||
|
self.user = get_user_model().objects.create_user(
|
||||||
|
username="layer-farmer",
|
||||||
|
password="secret123",
|
||||||
|
email="layer@example.com",
|
||||||
|
phone_number="09120000002",
|
||||||
|
)
|
||||||
|
self.farm_type = FarmType.objects.create(name="باغی")
|
||||||
|
self.farm = FarmHub.objects.create(owner=self.user, name="layer-farm", farm_type=self.farm_type)
|
||||||
|
|
||||||
|
def _create_area(self, **kwargs):
|
||||||
|
defaults = {
|
||||||
|
"farm": self.farm,
|
||||||
|
"geometry": AREA_GEOJSON,
|
||||||
|
"points": AREA_GEOJSON["geometry"]["coordinates"][0][:-1],
|
||||||
|
"center": {"longitude": 51.40874867, "latitude": 35.69575533},
|
||||||
|
"area_sqm": 300000,
|
||||||
|
"area_hectares": 30,
|
||||||
|
"chunk_area_sqm": 200000,
|
||||||
|
"zone_count": 1,
|
||||||
|
}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return CropArea.objects.create(**defaults)
|
||||||
|
|
||||||
|
def _create_completed_zone(self):
|
||||||
|
crop_area = self._create_area()
|
||||||
|
CropZone.objects.create(
|
||||||
|
crop_area=crop_area,
|
||||||
|
zone_id="zone-0",
|
||||||
|
geometry=AREA_GEOJSON["geometry"],
|
||||||
|
points=AREA_GEOJSON["geometry"]["coordinates"][0][:-1],
|
||||||
|
center={"longitude": 51.4087, "latitude": 35.6957},
|
||||||
|
area_sqm=300000,
|
||||||
|
area_hectares=30,
|
||||||
|
sequence=0,
|
||||||
|
processing_status=CropZone.STATUS_COMPLETED,
|
||||||
|
task_id="celery-task-1",
|
||||||
|
)
|
||||||
|
return crop_area
|
||||||
|
|
||||||
|
def _request(self, path):
|
||||||
|
request = self.factory.get(f"{path}?farm_uuid={self.farm.farm_uuid}")
|
||||||
|
force_authenticate(request, user=self.user)
|
||||||
|
return request
|
||||||
|
|
||||||
|
def test_water_need_view_requires_farm_uuid(self):
|
||||||
|
request = self.factory.get("/api/crop-zoning/water-need/")
|
||||||
|
force_authenticate(request, user=self.user)
|
||||||
|
|
||||||
|
response = WaterNeedView.as_view()(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.data["message"], "farm_uuid is required.")
|
||||||
|
|
||||||
|
def test_water_need_view_returns_area_style_payload(self):
|
||||||
|
self._create_completed_zone()
|
||||||
|
|
||||||
|
response = WaterNeedView.as_view()(self._request("/api/crop-zoning/water-need/"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["data"]["task"]["status"], "SUCCESS")
|
||||||
|
self.assertEqual(response.data["data"]["area"], AREA_GEOJSON)
|
||||||
|
self.assertEqual(len(response.data["data"]["zones"]), 1)
|
||||||
|
self.assertIn("waterNeedLayer", response.data["data"]["zones"][0])
|
||||||
|
self.assertNotIn("soilQualityLayer", response.data["data"]["zones"][0])
|
||||||
|
self.assertNotIn("cultivationRiskLayer", response.data["data"]["zones"][0])
|
||||||
|
|
||||||
|
def test_soil_quality_view_returns_area_style_payload(self):
|
||||||
|
self._create_completed_zone()
|
||||||
|
|
||||||
|
response = SoilQualityView.as_view()(self._request("/api/crop-zoning/soil-quality/"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["data"]["task"]["status"], "SUCCESS")
|
||||||
|
self.assertEqual(len(response.data["data"]["zones"]), 1)
|
||||||
|
self.assertIn("soilQualityLayer", response.data["data"]["zones"][0])
|
||||||
|
self.assertNotIn("waterNeedLayer", response.data["data"]["zones"][0])
|
||||||
|
self.assertNotIn("cultivationRiskLayer", response.data["data"]["zones"][0])
|
||||||
|
|
||||||
|
def test_cultivation_risk_view_returns_area_style_payload(self):
|
||||||
|
self._create_completed_zone()
|
||||||
|
|
||||||
|
response = CultivationRiskView.as_view()(self._request("/api/crop-zoning/cultivation-risk/"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["data"]["task"]["status"], "SUCCESS")
|
||||||
|
self.assertEqual(len(response.data["data"]["zones"]), 1)
|
||||||
|
self.assertIn("cultivationRiskLayer", response.data["data"]["zones"][0])
|
||||||
|
self.assertNotIn("waterNeedLayer", response.data["data"]["zones"][0])
|
||||||
|
self.assertNotIn("soilQualityLayer", response.data["data"]["zones"][0])
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ from django.urls import path
|
|||||||
|
|
||||||
from .views import (
|
from .views import (
|
||||||
AreaView,
|
AreaView,
|
||||||
|
CultivationRiskView,
|
||||||
ProductsView,
|
ProductsView,
|
||||||
|
SoilQualityView,
|
||||||
|
WaterNeedView,
|
||||||
ZoneDetailsView,
|
ZoneDetailsView,
|
||||||
ZonesCultivationRiskView,
|
ZonesCultivationRiskView,
|
||||||
ZonesInitialView,
|
ZonesInitialView,
|
||||||
@@ -12,6 +15,9 @@ from .views import (
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("area/", AreaView.as_view(), name="crop-zoning-area"),
|
path("area/", AreaView.as_view(), name="crop-zoning-area"),
|
||||||
|
path("water-need/", WaterNeedView.as_view(), name="crop-zoning-water-need"),
|
||||||
|
path("soil-quality/", SoilQualityView.as_view(), name="crop-zoning-soil-quality"),
|
||||||
|
path("cultivation-risk/", CultivationRiskView.as_view(), name="crop-zoning-cultivation-risk"),
|
||||||
path("products/", ProductsView.as_view(), name="crop-zoning-products"),
|
path("products/", ProductsView.as_view(), name="crop-zoning-products"),
|
||||||
# path("zones/initial/", ZonesInitialView.as_view(), name="crop-zoning-zones-initial"),
|
# path("zones/initial/", ZonesInitialView.as_view(), name="crop-zoning-zones-initial"),
|
||||||
# path(
|
# path(
|
||||||
|
|||||||
+75
-12
@@ -10,10 +10,13 @@ from config.swagger import status_response
|
|||||||
from .services import (
|
from .services import (
|
||||||
create_zones_and_dispatch,
|
create_zones_and_dispatch,
|
||||||
ensure_latest_area_ready_for_processing,
|
ensure_latest_area_ready_for_processing,
|
||||||
|
get_latest_cultivation_risk_payload,
|
||||||
get_cultivation_risk_payload,
|
get_cultivation_risk_payload,
|
||||||
get_default_area_feature,
|
get_default_area_feature,
|
||||||
get_initial_zones_payload,
|
get_initial_zones_payload,
|
||||||
get_latest_area_payload,
|
get_latest_area_payload,
|
||||||
|
get_latest_soil_quality_payload,
|
||||||
|
get_latest_water_need_payload,
|
||||||
get_products_payload,
|
get_products_payload,
|
||||||
get_soil_quality_payload,
|
get_soil_quality_payload,
|
||||||
get_water_need_payload,
|
get_water_need_payload,
|
||||||
@@ -22,10 +25,7 @@ from .services import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AreaView(APIView):
|
AREA_QUERY_PARAMETERS = [
|
||||||
@extend_schema(
|
|
||||||
tags=["Crop Zoning"],
|
|
||||||
parameters=[
|
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="farm_uuid",
|
name="farm_uuid",
|
||||||
type=OpenApiTypes.UUID,
|
type=OpenApiTypes.UUID,
|
||||||
@@ -47,13 +47,12 @@ class AreaView(APIView):
|
|||||||
required=False,
|
required=False,
|
||||||
description="تعداد زون در هر صفحه. مقدار پيش فرض 10 است.",
|
description="تعداد زون در هر صفحه. مقدار پيش فرض 10 است.",
|
||||||
),
|
),
|
||||||
],
|
]
|
||||||
responses={
|
|
||||||
200: status_response("CropZoningAreaResponse", data=serializers.JSONField()),
|
|
||||||
400: status_response("CropZoningAreaValidationError", data=serializers.JSONField()),
|
class BaseAreaDataView(APIView):
|
||||||
500: status_response("CropZoningAreaServerError", data=serializers.JSONField()),
|
payload_getter = None
|
||||||
},
|
|
||||||
)
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
farm_uuid = request.query_params.get("farm_uuid")
|
farm_uuid = request.query_params.get("farm_uuid")
|
||||||
try:
|
try:
|
||||||
@@ -65,11 +64,75 @@ class AreaView(APIView):
|
|||||||
return Response({"status": "error", "message": str(exc)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({"status": "error", "message": str(exc)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{"status": "success", "data": get_latest_area_payload(crop_area, page=page, page_size=page_size)},
|
{"status": "success", "data": self.payload_getter(crop_area, page=page, page_size=page_size)},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AreaView(BaseAreaDataView):
|
||||||
|
payload_getter = staticmethod(get_latest_area_payload)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Crop Zoning"],
|
||||||
|
parameters=AREA_QUERY_PARAMETERS,
|
||||||
|
responses={
|
||||||
|
200: status_response("CropZoningAreaResponse", data=serializers.JSONField()),
|
||||||
|
400: status_response("CropZoningAreaValidationError", data=serializers.JSONField()),
|
||||||
|
500: status_response("CropZoningAreaServerError", data=serializers.JSONField()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
return super().get(request)
|
||||||
|
|
||||||
|
|
||||||
|
class WaterNeedView(BaseAreaDataView):
|
||||||
|
payload_getter = staticmethod(get_latest_water_need_payload)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Crop Zoning"],
|
||||||
|
parameters=AREA_QUERY_PARAMETERS,
|
||||||
|
responses={
|
||||||
|
200: status_response("CropZoningWaterNeedResponse", data=serializers.JSONField()),
|
||||||
|
400: status_response("CropZoningWaterNeedValidationError", data=serializers.JSONField()),
|
||||||
|
500: status_response("CropZoningWaterNeedServerError", data=serializers.JSONField()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
return super().get(request)
|
||||||
|
|
||||||
|
|
||||||
|
class SoilQualityView(BaseAreaDataView):
|
||||||
|
payload_getter = staticmethod(get_latest_soil_quality_payload)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Crop Zoning"],
|
||||||
|
parameters=AREA_QUERY_PARAMETERS,
|
||||||
|
responses={
|
||||||
|
200: status_response("CropZoningSoilQualityResponse", data=serializers.JSONField()),
|
||||||
|
400: status_response("CropZoningSoilQualityValidationError", data=serializers.JSONField()),
|
||||||
|
500: status_response("CropZoningSoilQualityServerError", data=serializers.JSONField()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
return super().get(request)
|
||||||
|
|
||||||
|
|
||||||
|
class CultivationRiskView(BaseAreaDataView):
|
||||||
|
payload_getter = staticmethod(get_latest_cultivation_risk_payload)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Crop Zoning"],
|
||||||
|
parameters=AREA_QUERY_PARAMETERS,
|
||||||
|
responses={
|
||||||
|
200: status_response("CropZoningCultivationRiskResponse", data=serializers.JSONField()),
|
||||||
|
400: status_response("CropZoningCultivationRiskValidationError", data=serializers.JSONField()),
|
||||||
|
500: status_response("CropZoningCultivationRiskServerError", data=serializers.JSONField()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
return super().get(request)
|
||||||
|
|
||||||
|
|
||||||
class ProductsView(APIView):
|
class ProductsView(APIView):
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Crop Zoning"],
|
tags=["Crop Zoning"],
|
||||||
|
|||||||
Reference in New Issue
Block a user