UPDATE
This commit is contained in:
+5
-117
@@ -66,26 +66,6 @@ def reset_config():
|
||||
# 4.1 farmOverviewKpis
|
||||
FARM_OVERVIEW_KPIS = {
|
||||
"kpis": [
|
||||
{
|
||||
"id": "farm_health_score",
|
||||
"title": "امتیاز سلامت مزرعه",
|
||||
"subtitle": "تحلیل هوشمند",
|
||||
"stats": "87%",
|
||||
"avatarColor": "success",
|
||||
"avatarIcon": "tabler-heartbeat",
|
||||
"chipText": "خوب",
|
||||
"chipColor": "success",
|
||||
},
|
||||
{
|
||||
"id": "water_stress_index",
|
||||
"title": "شاخص تنش آبی",
|
||||
"subtitle": "فعلی",
|
||||
"stats": "12%",
|
||||
"avatarColor": "info",
|
||||
"avatarIcon": "tabler-droplet",
|
||||
"chipText": "پایین",
|
||||
"chipColor": "success",
|
||||
},
|
||||
{
|
||||
"id": "disease_risk",
|
||||
"title": "ریسک بیماری",
|
||||
@@ -96,16 +76,6 @@ FARM_OVERVIEW_KPIS = {
|
||||
"chipText": "5%",
|
||||
"chipColor": "success",
|
||||
},
|
||||
{
|
||||
"id": "avg_soil_moisture",
|
||||
"title": "میانگین رطوبت خاک",
|
||||
"subtitle": "کل مزرعه",
|
||||
"stats": "65%",
|
||||
"avatarColor": "primary",
|
||||
"avatarIcon": "tabler-plant-2",
|
||||
"chipText": "بهینه",
|
||||
"chipColor": "success",
|
||||
},
|
||||
{
|
||||
"id": "yield_prediction",
|
||||
"title": "پیشبینی عملکرد",
|
||||
@@ -231,47 +201,6 @@ SENSOR_VALUES_LIST = {
|
||||
]
|
||||
}
|
||||
|
||||
# 4.5 sensorRadarChart
|
||||
SENSOR_RADAR_CHART = {
|
||||
"labels": ["دما", "رطوبت", "pH", "هدایت الکتریکی", "نور", "باد"],
|
||||
"series": [
|
||||
{"name": "امروز", "data": [75, 65, 80, 70, 85, 60]},
|
||||
{"name": "ایدهآل", "data": [80, 70, 75, 75, 90, 50]},
|
||||
],
|
||||
}
|
||||
|
||||
# 4.6 sensorComparisonChart
|
||||
SENSOR_COMPARISON_CHART = {
|
||||
"currentValue": 48,
|
||||
"vsLastWeek": "+5%",
|
||||
"vsLastWeekValue": 5,
|
||||
"categories": ["دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه", "یکشنبه"],
|
||||
"series": [
|
||||
{"name": "امروز", "data": [42, 45, 48, 52, 50, 48, 46]},
|
||||
{"name": "هفته قبل", "data": [38, 40, 42, 45, 43, 40, 38]},
|
||||
],
|
||||
}
|
||||
|
||||
# 4.7 anomalyDetectionCard
|
||||
ANOMALY_DETECTION_CARD = {
|
||||
"anomalies": [
|
||||
{
|
||||
"sensor": "رطوبت خاک زون ۳",
|
||||
"value": "38%",
|
||||
"expected": "45-65%",
|
||||
"deviation": "-12%",
|
||||
"severity": "warning",
|
||||
},
|
||||
{
|
||||
"sensor": "pH بخش ۲",
|
||||
"value": "5.2",
|
||||
"expected": "6.0-7.0",
|
||||
"deviation": "-0.8",
|
||||
"severity": "error",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
# 4.8 farmAlertsTimeline
|
||||
FARM_ALERTS_TIMELINE = {
|
||||
"alerts": [
|
||||
@@ -345,47 +274,6 @@ YIELD_PREDICTION_CHART = {
|
||||
],
|
||||
}
|
||||
|
||||
# 4.12 soilMoistureHeatmap
|
||||
SOIL_MOISTURE_HEATMAP = {
|
||||
"zones": ["زون ۱", "زون ۲", "زون ۳", "زون ۴", "زون ۵", "زون ۶", "زون ۷"],
|
||||
"hours": ["۶ ص", "۸ ص", "۱۰ ص", "۱۲ ظ", "۱۴ ع", "۱۶ ع", "۱۸ ع"],
|
||||
"series": [
|
||||
{
|
||||
"name": "زون ۱",
|
||||
"data": [
|
||||
{"x": "۶ ص", "y": 52},
|
||||
{"x": "۸ ص", "y": 48},
|
||||
{"x": "۱۰ ص", "y": 55},
|
||||
{"x": "۱۲ ظ", "y": 60},
|
||||
{"x": "۱۴ ع", "y": 58},
|
||||
{"x": "۱۶ ع", "y": 54},
|
||||
{"x": "۱۸ ع", "y": 50},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "زون ۲",
|
||||
"data": [
|
||||
{"x": "۶ ص", "y": 45},
|
||||
{"x": "۸ ص", "y": 42},
|
||||
{"x": "۱۰ ص", "y": 48},
|
||||
{"x": "۱۲ ظ", "y": 52},
|
||||
{"x": "۱۴ ع", "y": 50},
|
||||
{"x": "۱۶ ع", "y": 47},
|
||||
{"x": "۱۸ ع", "y": 44},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# 4.13 ndviHealthCard
|
||||
NDVI_HEALTH_CARD = {
|
||||
"ndviIndex": 0.78,
|
||||
"healthData": [
|
||||
{"title": "تنش نیتروژن", "value": "پایین", "color": "success", "icon": "tabler-leaf"},
|
||||
{"title": "سلامت محصول", "value": "خوب", "color": "success", "icon": "tabler-plant"},
|
||||
],
|
||||
}
|
||||
|
||||
# 4.14 recommendationsList
|
||||
RECOMMENDATIONS_LIST = {
|
||||
"recommendations": [
|
||||
@@ -461,15 +349,15 @@ ALL_CARDS = {
|
||||
"farmWeatherCard": FARM_WEATHER_CARD, # هروز
|
||||
"farmAlertsTracker": FARM_ALERTS_TRACKER, #هروز
|
||||
"sensorValuesList": SENSOR_VALUES_LIST,#هروز
|
||||
"sensorRadarChart": SENSOR_RADAR_CHART,
|
||||
"sensorComparisonChart": SENSOR_COMPARISON_CHART,
|
||||
"anomalyDetectionCard": ANOMALY_DETECTION_CARD,
|
||||
"sensorRadarChart": {},
|
||||
"sensorComparisonChart": {},
|
||||
"anomalyDetectionCard": {},
|
||||
"farmAlertsTimeline": FARM_ALERTS_TIMELINE,
|
||||
"waterNeedPrediction": WATER_NEED_PREDICTION,
|
||||
"harvestPredictionCard": HARVEST_PREDICTION_CARD,
|
||||
"yieldPredictionChart": YIELD_PREDICTION_CHART,
|
||||
"soilMoistureHeatmap": SOIL_MOISTURE_HEATMAP,
|
||||
"ndviHealthCard": NDVI_HEALTH_CARD,
|
||||
"soilMoistureHeatmap": {},
|
||||
"ndviHealthCard": {},
|
||||
"recommendationsList": RECOMMENDATIONS_LIST, # این باید حتما از recommendetion ها گرفته بشه
|
||||
"economicOverview": ECONOMIC_OVERVIEW,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from water.services import (
|
||||
get_farm_weather_card_data,
|
||||
get_water_need_prediction_data,
|
||||
get_water_stress_index_data,
|
||||
)
|
||||
from crop_health.services import get_crop_health_summary_data
|
||||
from economic_overview.services import get_economic_overview_data
|
||||
from farm_alerts.services import (
|
||||
get_alert_timeline_data,
|
||||
get_alert_tracker_data,
|
||||
get_recommendations_list_data,
|
||||
)
|
||||
from fertilization_recommendation.services import get_fertilization_dashboard_recommendation
|
||||
from irrigation_recommendation.services import get_irrigation_dashboard_recommendation
|
||||
from pest_detection.services import get_risk_summary_data
|
||||
from sensor_7_in_1.services import (
|
||||
get_sensor_7_in_1_summary_data,
|
||||
)
|
||||
from yield_harvest.services import get_yield_harvest_summary_data
|
||||
|
||||
from .mock_data import ALL_CARDS
|
||||
|
||||
|
||||
def _update_kpi(card_lookup, card_data):
|
||||
if not card_data:
|
||||
return
|
||||
|
||||
card_id = card_data.get("id")
|
||||
if not card_id or card_id not in card_lookup:
|
||||
return
|
||||
|
||||
details = card_data.get("details")
|
||||
clean_data = {key: value for key, value in card_data.items() if key != "details"}
|
||||
card_lookup[card_id].update(clean_data)
|
||||
if details is not None:
|
||||
card_lookup[card_id]["details"] = details
|
||||
|
||||
|
||||
def _build_overview_kpis(base_cards, crop_health_summary, water_stress_index, avg_soil_moisture, risk_summary, yield_summary):
|
||||
kpis = [crop_health_summary["farmHealthScore"], water_stress_index, avg_soil_moisture, *deepcopy(base_cards["kpis"])]
|
||||
card_lookup = {item["id"]: item for item in kpis}
|
||||
|
||||
_update_kpi(card_lookup, water_stress_index)
|
||||
_update_kpi(card_lookup, avg_soil_moisture)
|
||||
_update_kpi(card_lookup, risk_summary.get("disease_risk", {}))
|
||||
_update_kpi(card_lookup, risk_summary.get("pest_risk", {}))
|
||||
_update_kpi(card_lookup, yield_summary.get("yield_prediction_card", {}))
|
||||
|
||||
return {"kpis": kpis}
|
||||
|
||||
|
||||
def _build_recommendations_list(farm, fallback_data, harvest_card):
|
||||
recommendations = []
|
||||
recommendations.extend(get_recommendations_list_data(farm).get("recommendations", []))
|
||||
recommendations.append(get_irrigation_dashboard_recommendation(farm))
|
||||
recommendations.append(get_fertilization_dashboard_recommendation(farm))
|
||||
|
||||
if harvest_card:
|
||||
recommendations.append(
|
||||
{
|
||||
"title": f"بازه برداشت: {harvest_card.get('optimalWindowStart', '')} تا {harvest_card.get('optimalWindowEnd', '')}",
|
||||
"subtitle": harvest_card.get("description", ""),
|
||||
"avatarIcon": "tabler-calendar-event",
|
||||
"avatarColor": "info",
|
||||
}
|
||||
)
|
||||
|
||||
deduped = []
|
||||
seen_titles = set()
|
||||
for item in recommendations:
|
||||
title = item.get("title")
|
||||
if not title or title in seen_titles:
|
||||
continue
|
||||
seen_titles.add(title)
|
||||
deduped.append(item)
|
||||
|
||||
if deduped:
|
||||
return {"recommendations": deduped[:4]}
|
||||
|
||||
return deepcopy(fallback_data)
|
||||
|
||||
|
||||
def get_farm_dashboard_cards(farm):
|
||||
cards = deepcopy(ALL_CARDS)
|
||||
|
||||
weather_card = get_farm_weather_card_data(farm)
|
||||
crop_health_summary = get_crop_health_summary_data(farm)
|
||||
risk_summary = get_risk_summary_data(farm)
|
||||
yield_summary = get_yield_harvest_summary_data(farm)
|
||||
water_stress_index = get_water_stress_index_data(farm)
|
||||
sensor_summary = get_sensor_7_in_1_summary_data(farm)
|
||||
avg_soil_moisture = sensor_summary["avgSoilMoisture"]
|
||||
|
||||
cards["farmWeatherCard"] = weather_card
|
||||
cards["farmAlertsTracker"] = get_alert_tracker_data(farm)
|
||||
cards["farmAlertsTimeline"] = get_alert_timeline_data(farm)
|
||||
cards["sensorValuesList"] = sensor_summary["sensorValuesList"]
|
||||
cards["anomalyDetectionCard"] = sensor_summary["anomalyDetectionCard"]
|
||||
cards["waterNeedPrediction"] = get_water_need_prediction_data(farm)
|
||||
cards["harvestPredictionCard"] = yield_summary["harvest_prediction_card"]
|
||||
cards["yieldPredictionChart"] = yield_summary["yield_prediction_chart"]
|
||||
cards["sensorRadarChart"] = sensor_summary["sensorRadarChart"]
|
||||
cards["sensorComparisonChart"] = sensor_summary["sensorComparisonChart"]
|
||||
cards["soilMoistureHeatmap"] = sensor_summary["soilMoistureHeatmap"]
|
||||
cards["ndviHealthCard"] = crop_health_summary["ndviHealthCard"]
|
||||
cards["economicOverview"] = get_economic_overview_data(farm)
|
||||
cards["farmOverviewKpis"] = _build_overview_kpis(
|
||||
cards["farmOverviewKpis"],
|
||||
crop_health_summary,
|
||||
water_stress_index,
|
||||
avg_soil_moisture,
|
||||
risk_summary,
|
||||
yield_summary,
|
||||
)
|
||||
cards["recommendationsList"] = _build_recommendations_list(
|
||||
farm,
|
||||
cards["recommendationsList"],
|
||||
yield_summary.get("harvest_prediction_card", {}),
|
||||
)
|
||||
|
||||
return cards
|
||||
+12
-12
@@ -1,5 +1,4 @@
|
||||
from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
@@ -136,23 +135,24 @@ class FarmDashboardConfigViewTests(DashboardBaseTestCase):
|
||||
|
||||
|
||||
class FarmDashboardCardsViewTests(DashboardBaseTestCase):
|
||||
@patch("dashboard.views.external_api_request")
|
||||
def test_get_forwards_farm_uuid_to_external_api(self, mock_external_api_request):
|
||||
mock_external_api_request.return_value.data = {"status": "success", "data": {}}
|
||||
mock_external_api_request.return_value.status_code = 200
|
||||
|
||||
def test_get_returns_locally_aggregated_cards(self):
|
||||
request = self.factory.get(f"/api/farm-dashboard/?farm_uuid={self.farm.farm_uuid}")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = FarmDashboardCardsView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_external_api_request.assert_called_once_with(
|
||||
"ai",
|
||||
"/dashboard-data/status",
|
||||
method="GET",
|
||||
query={"farm_uuid": str(self.farm.farm_uuid)},
|
||||
)
|
||||
self.assertEqual(response.data["code"], 200)
|
||||
self.assertEqual(response.data["msg"], "OK")
|
||||
self.assertIn("farmWeatherCard", response.data["data"])
|
||||
self.assertIn("farmAlertsTracker", response.data["data"])
|
||||
self.assertIn("yieldPredictionChart", response.data["data"])
|
||||
self.assertIn("ndviHealthCard", response.data["data"])
|
||||
self.assertIn("sensorRadarChart", response.data["data"])
|
||||
self.assertIn("soilMoistureHeatmap", response.data["data"])
|
||||
self.assertIn("economicOverview", response.data["data"])
|
||||
self.assertEqual(response.data["data"]["farmOverviewKpis"]["kpis"][0]["id"], "farm_health_score")
|
||||
self.assertEqual(response.data["data"]["farmOverviewKpis"]["kpis"][2]["id"], "avg_soil_moisture")
|
||||
|
||||
def test_get_requires_farm_uuid(self):
|
||||
request = self.factory.get("/api/farm-dashboard/")
|
||||
|
||||
+5
-8
@@ -10,8 +10,8 @@ from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
|
||||
|
||||
from config.swagger import code_response
|
||||
from external_api_adapter import request as external_api_request
|
||||
from farm_hub.models import FarmHub
|
||||
from .services import get_farm_dashboard_cards
|
||||
from .mock_data import DEFAULT_CONFIG
|
||||
from .models import FarmDashboardConfig
|
||||
from .serializers import FarmDashboardConfigPatchSerializer, FarmDashboardConfigSerializer
|
||||
@@ -117,7 +117,7 @@ class FarmDashboardConfigView(FarmAccessMixin, APIView):
|
||||
class FarmDashboardCardsView(FarmAccessMixin, APIView):
|
||||
"""
|
||||
Farm dashboard cards endpoint: GET.
|
||||
Requires farm_uuid and forwards it to the external AI service.
|
||||
Requires farm_uuid and assembles local dashboard services.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
@@ -125,10 +125,7 @@ class FarmDashboardCardsView(FarmAccessMixin, APIView):
|
||||
|
||||
def get(self, request):
|
||||
farm = self._get_farm(request, request.query_params.get("farm_uuid"))
|
||||
adapter_response = external_api_request(
|
||||
"ai",
|
||||
"/dashboard-data/status",
|
||||
method="GET",
|
||||
query={"farm_uuid": str(farm.farm_uuid)},
|
||||
return Response(
|
||||
{"code": 200, "msg": "OK", "data": get_farm_dashboard_cards(farm)},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(adapter_response.data, status=adapter_response.status_code)
|
||||
|
||||
Reference in New Issue
Block a user