UPDATE
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("farm_hub", "0002_seed_default_catalog"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="FarmDashboardConfig",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("disabled_card_ids", models.JSONField(blank=True, default=list)),
|
||||
("row_order", models.JSONField(default=list)),
|
||||
("enable_drag_reorder", models.BooleanField(default=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"farm",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="dashboard_config",
|
||||
to="farm_hub.farmhub",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "farm_dashboard_configs",
|
||||
"ordering": ["-updated_at", "-id"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.db import models
|
||||
|
||||
from farm_hub.models import FarmHub
|
||||
|
||||
|
||||
class FarmDashboardConfig(models.Model):
|
||||
farm = models.OneToOneField(
|
||||
FarmHub,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="dashboard_config",
|
||||
)
|
||||
disabled_card_ids = models.JSONField(default=list, blank=True)
|
||||
row_order = models.JSONField(default=list, blank=True)
|
||||
enable_drag_reorder = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "farm_dashboard_configs"
|
||||
ordering = ["-updated_at", "-id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"Dashboard config for {self.farm.name}"
|
||||
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ from .mock_data import VALID_CARD_IDS, VALID_ROW_IDS
|
||||
|
||||
|
||||
class FarmDashboardConfigSerializer(serializers.Serializer):
|
||||
farm_uuid = serializers.UUIDField(read_only=True)
|
||||
disabled_card_ids = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
allow_empty=True,
|
||||
@@ -40,6 +41,7 @@ class FarmDashboardConfigSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class FarmDashboardConfigPatchSerializer(FarmDashboardConfigSerializer):
|
||||
farm_uuid = serializers.UUIDField(required=True)
|
||||
disabled_card_ids = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
allow_empty=True,
|
||||
@@ -54,6 +56,6 @@ class FarmDashboardConfigPatchSerializer(FarmDashboardConfigSerializer):
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
if not attrs:
|
||||
if set(attrs.keys()) == {"farm_uuid"}:
|
||||
raise serializers.ValidationError("At least one config field must be provided.")
|
||||
return attrs
|
||||
|
||||
+100
-14
@@ -1,52 +1,105 @@
|
||||
from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
|
||||
from .mock_data import DEFAULT_CONFIG, reset_config
|
||||
from .views import FarmDashboardConfigView
|
||||
from farm_hub.models import FarmHub, FarmType
|
||||
|
||||
from .mock_data import DEFAULT_CONFIG
|
||||
from .models import FarmDashboardConfig
|
||||
from .views import FarmDashboardCardsView, FarmDashboardConfigView
|
||||
|
||||
|
||||
class FarmDashboardConfigViewTests(SimpleTestCase):
|
||||
class DashboardBaseTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
reset_config()
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username="farmer",
|
||||
password="secret123",
|
||||
email="farmer@example.com",
|
||||
phone_number="09120000000",
|
||||
)
|
||||
self.other_user = get_user_model().objects.create_user(
|
||||
username="other-farmer",
|
||||
password="secret123",
|
||||
email="other@example.com",
|
||||
phone_number="09120000001",
|
||||
)
|
||||
self.farm_type = FarmType.objects.create(name="زراعی")
|
||||
self.farm = FarmHub.objects.create(owner=self.user, farm_type=self.farm_type, name="Farm 1")
|
||||
self.other_farm = FarmHub.objects.create(owner=self.other_user, farm_type=self.farm_type, name="Farm 2")
|
||||
|
||||
def tearDown(self):
|
||||
reset_config()
|
||||
|
||||
def test_get_returns_canonical_config(self):
|
||||
request = self.factory.get("/api/farm-dashboard-config/")
|
||||
class FarmDashboardConfigViewTests(DashboardBaseTestCase):
|
||||
def test_get_returns_default_config_and_persists_it(self):
|
||||
request = self.factory.get(f"/api/farm-dashboard-config/?farm_uuid={self.farm.farm_uuid}")
|
||||
force_authenticate(request, user=self.user)
|
||||
response = FarmDashboardConfigView.as_view()(request)
|
||||
|
||||
expected = deepcopy(DEFAULT_CONFIG)
|
||||
expected["farm_uuid"] = str(self.farm.farm_uuid)
|
||||
|
||||
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)
|
||||
self.assertEqual(response.data["data"], expected)
|
||||
self.assertTrue(FarmDashboardConfig.objects.filter(farm=self.farm).exists())
|
||||
|
||||
def test_get_requires_farm_uuid(self):
|
||||
request = self.factory.get("/api/farm-dashboard-config/")
|
||||
force_authenticate(request, user=self.user)
|
||||
response = FarmDashboardConfigView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.data["farm_uuid"][0], "This field is required.")
|
||||
|
||||
def test_get_rejects_foreign_farm_uuid(self):
|
||||
request = self.factory.get(f"/api/farm-dashboard-config/?farm_uuid={self.other_farm.farm_uuid}")
|
||||
force_authenticate(request, user=self.user)
|
||||
response = FarmDashboardConfigView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.data["farm_uuid"][0], "Farm not found.")
|
||||
|
||||
def test_patch_partial_update_returns_full_final_config(self):
|
||||
request = self.factory.patch(
|
||||
"/api/farm-dashboard-config/",
|
||||
{"disabled_card_ids": ["farmWeatherCard"]},
|
||||
{
|
||||
"farm_uuid": str(self.farm.farm_uuid),
|
||||
"disabled_card_ids": ["farmWeatherCard"],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
response = FarmDashboardConfigView.as_view()(request)
|
||||
|
||||
expected = deepcopy(DEFAULT_CONFIG)
|
||||
expected["farm_uuid"] = str(self.farm.farm_uuid)
|
||||
expected["disabled_card_ids"] = ["farmWeatherCard"]
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["data"], expected)
|
||||
self.assertEqual(
|
||||
FarmDashboardConfig.objects.get(farm=self.farm).disabled_card_ids,
|
||||
["farmWeatherCard"],
|
||||
)
|
||||
|
||||
def test_patch_only_drag_flag_still_returns_full_config(self):
|
||||
request = self.factory.patch(
|
||||
"/api/farm-dashboard-config/",
|
||||
{"enable_drag_reorder": False},
|
||||
{
|
||||
"farm_uuid": str(self.farm.farm_uuid),
|
||||
"enable_drag_reorder": False,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
response = FarmDashboardConfigView.as_view()(request)
|
||||
|
||||
expected = deepcopy(DEFAULT_CONFIG)
|
||||
expected["farm_uuid"] = str(self.farm.farm_uuid)
|
||||
expected["enable_drag_reorder"] = False
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -57,10 +110,43 @@ class FarmDashboardConfigViewTests(SimpleTestCase):
|
||||
def test_patch_rejects_invalid_row_order(self):
|
||||
request = self.factory.patch(
|
||||
"/api/farm-dashboard-config/",
|
||||
{"row_order": ["overviewKpis"]},
|
||||
{
|
||||
"farm_uuid": str(self.farm.farm_uuid),
|
||||
"row_order": ["overviewKpis"],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
response = FarmDashboardConfigView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("row_order", response.data)
|
||||
|
||||
|
||||
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
|
||||
|
||||
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)},
|
||||
)
|
||||
|
||||
def test_get_requires_farm_uuid(self):
|
||||
request = self.factory.get("/api/farm-dashboard/")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = FarmDashboardCardsView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.data["farm_uuid"][0], "This field is required.")
|
||||
|
||||
+85
-20
@@ -2,21 +2,59 @@
|
||||
Farm Dashboard API views.
|
||||
"""
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework import serializers
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
|
||||
|
||||
from config.swagger import code_response
|
||||
from .mock_data import get_config, update_config
|
||||
from external_api_adapter import request as external_api_request
|
||||
from farm_hub.models import FarmHub
|
||||
from .mock_data import DEFAULT_CONFIG
|
||||
from .models import FarmDashboardConfig
|
||||
from .serializers import FarmDashboardConfigPatchSerializer, FarmDashboardConfigSerializer
|
||||
|
||||
|
||||
class FarmAccessMixin:
|
||||
@staticmethod
|
||||
def _get_farm(request, farm_uuid):
|
||||
if not farm_uuid:
|
||||
raise serializers.ValidationError({"farm_uuid": ["This field is required."]})
|
||||
try:
|
||||
return FarmHub.objects.get(farm_uuid=farm_uuid, owner=request.user)
|
||||
except FarmHub.DoesNotExist as exc:
|
||||
raise serializers.ValidationError({"farm_uuid": ["Farm not found."]}) from exc
|
||||
|
||||
@staticmethod
|
||||
def _get_or_create_dashboard_config(farm):
|
||||
config, _created = FarmDashboardConfig.objects.get_or_create(
|
||||
farm=farm,
|
||||
defaults={
|
||||
"disabled_card_ids": DEFAULT_CONFIG["disabled_card_ids"],
|
||||
"row_order": DEFAULT_CONFIG["row_order"],
|
||||
"enable_drag_reorder": DEFAULT_CONFIG["enable_drag_reorder"],
|
||||
},
|
||||
)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def _serialize_config(config):
|
||||
return {
|
||||
"farm_uuid": str(config.farm.farm_uuid),
|
||||
"disabled_card_ids": config.disabled_card_ids,
|
||||
"row_order": config.row_order,
|
||||
"enable_drag_reorder": config.enable_drag_reorder,
|
||||
}
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
tags=["Farm Dashboard"],
|
||||
parameters=[
|
||||
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True),
|
||||
],
|
||||
responses={200: code_response("FarmDashboardConfigGetResponse", data=FarmDashboardConfigSerializer())},
|
||||
),
|
||||
patch=extend_schema(
|
||||
@@ -25,25 +63,43 @@ from .serializers import FarmDashboardConfigPatchSerializer, FarmDashboardConfig
|
||||
responses={200: code_response("FarmDashboardConfigPatchResponse", data=FarmDashboardConfigSerializer())},
|
||||
),
|
||||
)
|
||||
class FarmDashboardConfigView(APIView):
|
||||
class FarmDashboardConfigView(FarmAccessMixin, APIView):
|
||||
"""
|
||||
Farm dashboard config endpoints.
|
||||
GET returns the current config.
|
||||
PATCH accepts partial updates and returns the full final config.
|
||||
GET/PATCH are persisted in DB per farm.
|
||||
"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
config = get_config()
|
||||
return Response({"code": 200, "msg": "OK", "data": config}, status=status.HTTP_200_OK)
|
||||
farm = self._get_farm(request, request.query_params.get("farm_uuid"))
|
||||
config = self._get_or_create_dashboard_config(farm)
|
||||
return Response(
|
||||
{"code": 200, "msg": "OK", "data": self._serialize_config(config)},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def patch(self, request):
|
||||
serializer = FarmDashboardConfigPatchSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
config = update_config(serializer.validated_data)
|
||||
response_serializer = FarmDashboardConfigSerializer(config)
|
||||
|
||||
farm = self._get_farm(request, serializer.validated_data["farm_uuid"])
|
||||
config = self._get_or_create_dashboard_config(farm)
|
||||
|
||||
update_fields = ["updated_at"]
|
||||
if "disabled_card_ids" in serializer.validated_data:
|
||||
config.disabled_card_ids = serializer.validated_data["disabled_card_ids"]
|
||||
update_fields.append("disabled_card_ids")
|
||||
if "row_order" in serializer.validated_data:
|
||||
config.row_order = serializer.validated_data["row_order"]
|
||||
update_fields.append("row_order")
|
||||
if "enable_drag_reorder" in serializer.validated_data:
|
||||
config.enable_drag_reorder = serializer.validated_data["enable_drag_reorder"]
|
||||
update_fields.append("enable_drag_reorder")
|
||||
config.save(update_fields=update_fields)
|
||||
|
||||
return Response(
|
||||
{"code": 200, "msg": "OK", "data": response_serializer.data},
|
||||
{"code": 200, "msg": "OK", "data": self._serialize_config(config)},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -51,17 +107,26 @@ class FarmDashboardConfigView(APIView):
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
tags=["Farm Dashboard"],
|
||||
parameters=[
|
||||
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True),
|
||||
],
|
||||
responses={200: code_response("FarmDashboardCardsResponse", data=serializers.JSONField())},
|
||||
),
|
||||
)
|
||||
class FarmDashboardCardsView(APIView):
|
||||
class FarmDashboardCardsView(FarmAccessMixin, APIView):
|
||||
"""
|
||||
Farm dashboard cards endpoint: GET.
|
||||
Returns unified response with all 15 card payloads.
|
||||
No database. Static mock data only.
|
||||
Requires farm_uuid and forwards it to the external AI service.
|
||||
"""
|
||||
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")
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
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(adapter_response.data, status=adapter_response.status_code)
|
||||
|
||||
Reference in New Issue
Block a user