This commit is contained in:
2026-04-04 01:16:16 +03:30
parent ecb42c6895
commit 6d5ece1f5d
22 changed files with 311 additions and 261 deletions
+9 -24
View File
@@ -3,6 +3,7 @@ import uuid
from django.db import transaction
from account.seeds import seed_admin_user
from sensor_catalog.management import seed_sensor_catalog
from sensor_catalog.models import SensorCatalog
from .catalog import CATALOG_SEED_DATA
@@ -16,32 +17,15 @@ ADMIN_FARM_DATA = {
"is_active": True,
"sensors": [
{
"sensor_catalog_name": "Sensor 7 - Soil Moisture Sensor v1.2",
"physical_device_uuid": uuid.UUID("22222222-2222-2222-2222-222222222221"),
"name": "Station 1",
"sensor_type": "weather_station",
"is_active": True,
"specifications": {
"model": "CL-SENSE-PRO-X",
"firmware": "2.4.1",
"manufacturer": "CropLogic",
},
"power_source": {
"type": "hybrid",
"battery": {"capacity_mah": 12000, "voltage": 12},
"solar": {"panel_watt": 40, "controller": "MPPT"},
},
},
{
"sensor_catalog_name": "Sensor 7 - Soil Moisture Sensor v1.2",
"sensor_catalog_code": "sensor_7_soil_moisture_sensor_v1_2",
"physical_device_uuid": uuid.UUID("22222222-2222-2222-2222-222222222222"),
"name": "Soil Probe 1",
"sensor_type": "soil_probe",
"is_active": True,
"specifications": {
"capabilities": ["soil_moisture", "soil_temperature", "ph", "ec"],
"capabilities": ["soil_moisture", "analog_output", "digital_output"],
},
"power_source": {"type": "battery", "backup": "solar"},
"power_source": {"type": "solar"},
},
],
}
@@ -81,12 +65,13 @@ def _get_default_catalog():
return FarmType.objects.get(name=default_farm_type_name), created_products[:2]
def _get_sensor_catalog_by_name(name):
return SensorCatalog.objects.filter(name=name).first()
def _get_sensor_catalog_by_code(code):
return SensorCatalog.objects.filter(code=code).first()
@transaction.atomic
def seed_admin_farm():
seed_sensor_catalog()
owner, _ = seed_admin_user()
farm_type, products = _get_default_catalog()
farm, created = FarmHub.objects.update_or_create(
@@ -103,8 +88,8 @@ def seed_admin_farm():
sensors = []
for sensor_data in ADMIN_FARM_DATA["sensors"]:
sensor_data = sensor_data.copy()
sensor_catalog_name = sensor_data.pop("sensor_catalog_name", None)
sensor_data["sensor_catalog"] = _get_sensor_catalog_by_name(sensor_catalog_name) if sensor_catalog_name else None
sensor_catalog_code = sensor_data.pop("sensor_catalog_code", None)
sensor_data["sensor_catalog"] = _get_sensor_catalog_by_code(sensor_catalog_code) if sensor_catalog_code else None
sensors.append(farm.sensors.model(farm=farm, **sensor_data))
farm.sensors.bulk_create(sensors)
if created:
+8 -1
View File
@@ -2,6 +2,7 @@ from rest_framework import serializers
from access_control.models import SubscriptionPlan
from access_control.serializers import SubscriptionPlanSerializer
from access_control.catalog import GOLD_PLAN_CODE
from access_control.services import get_effective_subscription_plan
from .models import FarmHub, FarmSensor, FarmType, Product
from sensor_catalog.models import SensorCatalog
@@ -58,7 +59,7 @@ class FarmSensorSerializer(serializers.ModelSerializer):
class FarmHubSerializer(serializers.ModelSerializer):
last_updated = serializers.DateTimeField(source="updated_at", read_only=True)
farm_type = FarmTypeSerializer(read_only=True)
subscription_plan = SubscriptionPlanSerializer(read_only=True)
subscription_plan = serializers.SerializerMethodField()
products = ProductSerializer(many=True, read_only=True)
sensors = FarmSensorSerializer(many=True, read_only=True)
area_uuid = serializers.UUIDField(source="current_crop_area.uuid", read_only=True)
@@ -78,6 +79,12 @@ class FarmHubSerializer(serializers.ModelSerializer):
]
read_only_fields = ["farm_uuid", "last_updated"]
def get_subscription_plan(self, obj):
subscription_plan = get_effective_subscription_plan(obj)
if subscription_plan is None:
return None
return SubscriptionPlanSerializer(subscription_plan, context=self.context).data
class FarmSensorWriteSerializer(serializers.ModelSerializer):
sensor_catalog_uuid = serializers.UUIDField(write_only=True, required=False)
+48 -16
View File
@@ -7,6 +7,7 @@ from access_control.services import build_farm_access_profile
from access_control.views import FarmAccessProfileView
from crop_zoning.models import CropArea
from farm_hub.models import FarmHub, FarmType, Product
from farm_hub.serializers import FarmHubSerializer
from farm_hub.seeds import seed_admin_farm
from farm_hub.views import FarmListCreateView, FarmTypeListView, FarmTypeProductsView
from sensor_catalog.models import SensorCatalog
@@ -46,6 +47,7 @@ class FarmListCreateViewTests(TestCase):
self.wheat, _ = Product.objects.get_or_create(farm_type=self.farm_type, name="گندم")
self.plan = SubscriptionPlan.objects.create(code="gold", name="Gold")
self.weather_station, _ = SensorCatalog.objects.get_or_create(
code="sensor_7_soil_moisture_sensor_v1_2",
name="Sensor 7 - Soil Moisture Sensor v1.2",
defaults={"supported_power_sources": ["solar", "direct_power"]},
)
@@ -159,23 +161,16 @@ class FarmListCreateViewTests(TestCase):
)
class FarmSeedTests(TestCase):
def test_seed_admin_farm_dispatches_crop_logic_flow_on_create(self):
SensorCatalog.objects.get_or_create(
name="Sensor 7 - Soil Moisture Sensor v1.2",
defaults={"supported_power_sources": ["solar", "direct_power"]},
)
farm, created = seed_admin_farm()
self.assertTrue(created)
self.assertEqual(farm.farm_uuid.hex, "11111111111111111111111111111111")
self.assertEqual(CropArea.objects.count(), 1)
self.assertEqual(farm.sensors.count(), 2)
self.assertEqual(farm.sensors.count(), 1)
self.assertIsNotNone(farm.sensors.first().physical_device_uuid)
self.assertTrue(SensorCatalog.objects.filter(code="sensor_7_soil_moisture_sensor_v1_2").exists())
def test_seed_admin_farm_does_not_dispatch_twice_for_existing_seed(self):
SensorCatalog.objects.get_or_create(
name="Sensor 7 - Soil Moisture Sensor v1.2",
defaults={"supported_power_sources": ["solar", "direct_power"]},
)
first_farm, first_created = seed_admin_farm()
second_farm, second_created = seed_admin_farm()
@@ -252,7 +247,7 @@ class FarmAccessProfileTests(TestCase):
self.plan = SubscriptionPlan.objects.create(code="starter", name="Starter")
self.farm_type = FarmType.objects.create(name="گلخانه ای")
self.product = Product.objects.create(farm_type=self.farm_type, name="خیار")
self.sensor_catalog = SensorCatalog.objects.create(name="Climate Sensor")
self.sensor_catalog = SensorCatalog.objects.create(code="climate_sensor", name="Climate Sensor")
self.farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
@@ -314,24 +309,61 @@ class FarmAccessProfileTests(TestCase):
response = FarmAccessProfileView.as_view()(request, farm_uuid=self.farm.farm_uuid)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["data"]["groups"]["pages"]["greenhouse-dashboard"]["enabled"], True)
self.assertEqual(response.data["data"]["groups"]["widgets"]["sensor-analytics"]["enabled"], True)
self.assertNotIn("features", response.data["data"])
self.assertNotIn("groups", response.data["data"])
self.assertEqual(len(response.data["data"]["matched_rules"]), 3)
self.assertTrue(FarmAccessProfile.objects.filter(farm=self.farm).exists())
def test_sensor_rule_can_match_by_metadata_sensor_name(self):
def test_sensor_rule_can_match_by_metadata_sensor_code(self):
sensor_page = AccessFeature.objects.create(
code="sensor-page",
name="Sensor Page",
feature_type=AccessFeature.PAGE,
)
sensor_rule = AccessRule.objects.create(
code="sensor-page-by-name",
name="Sensor Page By Name",
code="sensor-page-by-code",
name="Sensor Page By Code",
priority=40,
metadata={"sensor_catalog_names": [self.sensor_catalog.name]},
metadata={"sensor_catalog_codes": [self.sensor_catalog.code]},
)
sensor_rule.features.add(sensor_page)
profile = build_farm_access_profile(self.farm)
self.assertTrue(profile["features"]["sensor-page"]["enabled"])
def test_build_farm_access_profile_falls_back_to_default_plan(self):
default_plan = SubscriptionPlan.objects.create(code="gold", name="Gold", metadata={"is_default": True})
fallback_farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
subscription_plan=None,
name="Fallback Plan Farm",
)
fallback_farm.products.add(self.product)
fallback_feature = AccessFeature.objects.create(
code="fallback-dashboard",
name="Fallback Dashboard",
feature_type=AccessFeature.PAGE,
)
fallback_rule = AccessRule.objects.create(code="gold-fallback-rule", name="Gold Fallback Rule", priority=5)
fallback_rule.features.add(fallback_feature)
fallback_rule.subscription_plans.add(default_plan)
profile = build_farm_access_profile(fallback_farm)
self.assertEqual(profile["subscription_plan"]["code"], "gold")
self.assertTrue(profile["features"]["fallback-dashboard"]["enabled"])
def test_farm_serializer_returns_default_plan_when_model_plan_is_null(self):
SubscriptionPlan.objects.create(code="gold", name="Gold", metadata={"is_default": True})
fallback_farm = FarmHub.objects.create(
owner=self.user,
farm_type=self.farm_type,
subscription_plan=None,
name="Serializer Fallback Farm",
)
payload = FarmHubSerializer(fallback_farm).data
self.assertEqual(payload["subscription_plan"]["code"], "gold")