This commit is contained in:
2026-04-11 03:54:15 +03:30
parent 883573004c
commit 36d6b05a7f
68 changed files with 3487 additions and 841 deletions
+5 -117
View File
@@ -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,
}
+123
View File
@@ -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
View File
@@ -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
View File
@@ -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)