This commit is contained in:
2026-03-26 15:39:31 +03:30
parent f305e00cfe
commit 32a0e3f3d9
26 changed files with 2188 additions and 265 deletions
+59 -19
View File
@@ -1,28 +1,68 @@
"""
Static mock data for Farm Dashboard API.
No database, no dynamic values. Pure static payloads.
"""
# Config payload for GET/PATCH farm-dashboard-config (section 2.1)
# row_order must use valid row IDs only: overviewKpis, weatherAlerts, sensorMonitoring,
# sensorCharts, alertsWater, predictions, soilHeatmap, ndviRecommendations, economic
CONFIG = {
"disabled_card_ids": [
"predictions",
],
"row_order": [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"soilHeatmap",
"ndviRecommendations",
"economic",
],
"enable_drag_reorder": False,
from copy import deepcopy
from threading import Lock
VALID_ROW_IDS = [
"overviewKpis",
"weatherAlerts",
"sensorMonitoring",
"sensorCharts",
"alertsWater",
"predictions",
"soilHeatmap",
"ndviRecommendations",
"economic",
]
VALID_CARD_IDS = [
"farmOverviewKpis",
"farmWeatherCard",
"farmAlertsTracker",
"sensorValuesList",
"sensorRadarChart",
"sensorComparisonChart",
"anomalyDetectionCard",
"farmAlertsTimeline",
"waterNeedPrediction",
"harvestPredictionCard",
"yieldPredictionChart",
"soilMoistureHeatmap",
"ndviHealthCard",
"recommendationsList",
"economicOverview",
]
DEFAULT_CONFIG = {
"disabled_card_ids": [],
"row_order": VALID_ROW_IDS.copy(),
"enable_drag_reorder": True,
}
_config_lock = Lock()
_config_state = deepcopy(DEFAULT_CONFIG)
def get_config():
with _config_lock:
return deepcopy(_config_state)
def update_config(changes):
with _config_lock:
_config_state.update(deepcopy(changes))
return deepcopy(_config_state)
def reset_config():
with _config_lock:
_config_state.clear()
_config_state.update(deepcopy(DEFAULT_CONFIG))
return deepcopy(_config_state)
# 4.1 farmOverviewKpis
FARM_OVERVIEW_KPIS = {
"kpis": [
+59
View File
@@ -0,0 +1,59 @@
from rest_framework import serializers
from .mock_data import VALID_CARD_IDS, VALID_ROW_IDS
class FarmDashboardConfigSerializer(serializers.Serializer):
disabled_card_ids = serializers.ListField(
child=serializers.CharField(),
allow_empty=True,
)
row_order = serializers.ListField(
child=serializers.CharField(),
allow_empty=False,
)
enable_drag_reorder = serializers.BooleanField()
def validate_disabled_card_ids(self, value):
invalid_ids = [card_id for card_id in value if card_id not in VALID_CARD_IDS]
if invalid_ids:
raise serializers.ValidationError(
f"Invalid card IDs: {', '.join(invalid_ids)}."
)
if len(set(value)) != len(value):
raise serializers.ValidationError("disabled_card_ids must not contain duplicates.")
return value
def validate_row_order(self, value):
invalid_ids = [row_id for row_id in value if row_id not in VALID_ROW_IDS]
if invalid_ids:
raise serializers.ValidationError(
f"Invalid row IDs: {', '.join(invalid_ids)}."
)
if len(set(value)) != len(value):
raise serializers.ValidationError("row_order must not contain duplicates.")
if set(value) != set(VALID_ROW_IDS):
raise serializers.ValidationError(
"row_order must contain each valid row ID exactly once."
)
return value
class FarmDashboardConfigPatchSerializer(FarmDashboardConfigSerializer):
disabled_card_ids = serializers.ListField(
child=serializers.CharField(),
allow_empty=True,
required=False,
)
row_order = serializers.ListField(
child=serializers.CharField(),
allow_empty=False,
required=False,
)
enable_drag_reorder = serializers.BooleanField(required=False)
def validate(self, attrs):
attrs = super().validate(attrs)
if not attrs:
raise serializers.ValidationError("At least one config field must be provided.")
return attrs
+66
View File
@@ -0,0 +1,66 @@
from copy import deepcopy
from django.test import SimpleTestCase
from rest_framework.test import APIRequestFactory
from .mock_data import DEFAULT_CONFIG, reset_config
from .views import FarmDashboardConfigView
class FarmDashboardConfigViewTests(SimpleTestCase):
def setUp(self):
self.factory = APIRequestFactory()
reset_config()
def tearDown(self):
reset_config()
def test_get_returns_canonical_config(self):
request = self.factory.get("/api/farm-dashboard-config/")
response = FarmDashboardConfigView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["code"], 200)
self.assertEqual(response.data["msg"], "OK")
self.assertEqual(response.data["data"], DEFAULT_CONFIG)
def test_patch_partial_update_returns_full_final_config(self):
request = self.factory.patch(
"/api/farm-dashboard-config/",
{"disabled_card_ids": ["farmWeatherCard"]},
format="json",
)
response = FarmDashboardConfigView.as_view()(request)
expected = deepcopy(DEFAULT_CONFIG)
expected["disabled_card_ids"] = ["farmWeatherCard"]
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["data"], expected)
def test_patch_only_drag_flag_still_returns_full_config(self):
request = self.factory.patch(
"/api/farm-dashboard-config/",
{"enable_drag_reorder": False},
format="json",
)
response = FarmDashboardConfigView.as_view()(request)
expected = deepcopy(DEFAULT_CONFIG)
expected["enable_drag_reorder"] = False
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["data"], expected)
self.assertIn("disabled_card_ids", response.data["data"])
self.assertIn("row_order", response.data["data"])
def test_patch_rejects_invalid_row_order(self):
request = self.factory.patch(
"/api/farm-dashboard-config/",
{"row_order": ["overviewKpis"]},
format="json",
)
response = FarmDashboardConfigView.as_view()(request)
self.assertEqual(response.status_code, 400)
self.assertIn("row_order", response.data)
+1 -1
View File
@@ -3,6 +3,6 @@ from django.urls import path
from .views import FarmDashboardCardsView
urlpatterns = [
path("cards/", FarmDashboardCardsView.as_view(), name="farm-dashboard-cards"),
# path("cards/", FarmDashboardCardsView.as_view(), name="farm-dashboard-cards"),
path("", FarmDashboardCardsView.as_view(), name="farm-dashboard"),
]
+23 -13
View File
@@ -1,43 +1,51 @@
"""
Farm Dashboard API views.
No database connection. All responses use static mock data from mock_data.py.
"""
from rest_framework import status
from rest_framework import serializers
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, extend_schema_view
from config.swagger import code_response
from external_api_adapter import request as external_api_request
from .mock_data import CONFIG
from .mock_data import get_config, update_config
from .serializers import FarmDashboardConfigPatchSerializer, FarmDashboardConfigSerializer
@extend_schema_view(
get=extend_schema(
tags=["Farm Dashboard"],
responses={200: code_response("FarmDashboardConfigGetResponse", data=serializers.JSONField())},
responses={200: code_response("FarmDashboardConfigGetResponse", data=FarmDashboardConfigSerializer())},
),
patch=extend_schema(
tags=["Farm Dashboard"],
request=OpenApiTypes.OBJECT,
responses={200: code_response("FarmDashboardConfigPatchResponse", data=serializers.JSONField())},
request=FarmDashboardConfigPatchSerializer,
responses={200: code_response("FarmDashboardConfigPatchResponse", data=FarmDashboardConfigSerializer())},
),
)
class FarmDashboardConfigView(APIView):
"""
Farm dashboard config endpoints: GET and PATCH.
GET returns static config (disabled_card_ids, row_order, enable_drag_reorder).
PATCH accepts body but returns same static config; no processing or validation.
No database. No input values used in response.
Farm dashboard config endpoints.
GET returns the current config.
PATCH accepts partial updates and returns the full final config.
"""
permission_classes = [AllowAny]
def get(self, request):
return Response({"code": 200, "msg": "OK", "data": CONFIG}, status=status.HTTP_200_OK)
config = get_config()
return Response({"code": 200, "msg": "OK", "data": config}, status=status.HTTP_200_OK)
def patch(self, request):
return Response({"code": 200, "msg": "OK", "data": CONFIG}, status=status.HTTP_200_OK)
serializer = FarmDashboardConfigPatchSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
config = update_config(serializer.validated_data)
response_serializer = FarmDashboardConfigSerializer(config)
return Response(
{"code": 200, "msg": "OK", "data": response_serializer.data},
status=status.HTTP_200_OK,
)
@extend_schema_view(
@@ -53,5 +61,7 @@ class FarmDashboardCardsView(APIView):
No database. Static mock data only.
"""
def get(self, request):
from external_api_adapter import request as external_api_request
adapter_response = external_api_request("ai", "/dashboard-data/status", method="GET")
return Response(adapter_response.data, status=adapter_response.status_code)