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
+18 -16
View File
@@ -1,5 +1,6 @@
GOLD_PLAN_CODE = "gold"
SENSOR_7_NAME = "Sensor 7 - Soil Moisture Sensor v1.2"
SENSOR_7_CODE = "sensor_7_soil_moisture_sensor_v1_2"
DEFAULT_SUBSCRIPTION_PLANS = [
@@ -13,21 +14,21 @@ DEFAULT_SUBSCRIPTION_PLANS = [
DEFAULT_ACCESS_FEATURES = [
{"code": "dashboards", "name": "داشبوردها", "feature_type": "section"},
{"code": "data-section", "name": "بخش داده ها", "feature_type": "section"},
{"code": "water-data", "name": "دیتاهای آب", "feature_type": "page"},
{"code": "soil-information", "name": "اطلاعات خاک", "feature_type": "page"},
{"code": "crop-zoning", "name": "زون بندی کشت", "feature_type": "page"},
{"code": "simulator", "name": "شبیه ساز", "feature_type": "section"},
{"code": "plant-growth-simulator", "name": "شبیه ساز رشد گیاه", "feature_type": "page"},
{"code": "recommendations", "name": "توصیه ها", "feature_type": "section"},
{"code": "irrigation-recommendation", "name": "توصیه آبیاری", "feature_type": "page"},
{"code": "fertilization-recommendation", "name": "توصیه کوددهی", "feature_type": "page"},
{"code": "smart-assistant", "name": "دستیار هوشمند", "feature_type": "section"},
{"code": "farm-ai-assistant", "name": "دستیار هوشمند مزرعه", "feature_type": "page"},
{"code": "pest-detection", "name": "تشخیص آفات گیاهی", "feature_type": "page"},
{"code": "sensor-page", "name": "صفحه سنسور", "feature_type": "page"},
{"code": "greenhouse-dashboard", "name": "Greenhouse Dashboard", "feature_type": "page"},
{"code": "dashboards", "name": "داشبوردها", "feature_type": "section", "default_enabled": True},
{"code": "data-section", "name": "بخش داده ها", "feature_type": "section", "default_enabled": True},
{"code": "water-data", "name": "دیتاهای آب", "feature_type": "page", "default_enabled": True},
{"code": "soil-information", "name": "اطلاعات خاک", "feature_type": "page", "default_enabled": True},
{"code": "crop-zoning", "name": "زون بندی کشت", "feature_type": "page", "default_enabled": True},
{"code": "simulator", "name": "شبیه ساز", "feature_type": "section", "default_enabled": True},
{"code": "plant-growth-simulator", "name": "شبیه ساز رشد گیاه", "feature_type": "page", "default_enabled": True},
{"code": "recommendations", "name": "توصیه ها", "feature_type": "section", "default_enabled": True},
{"code": "irrigation-recommendation", "name": "توصیه آبیاری", "feature_type": "page", "default_enabled": True},
{"code": "fertilization-recommendation", "name": "توصیه کوددهی", "feature_type": "page", "default_enabled": True},
{"code": "smart-assistant", "name": "دستیار هوشمند", "feature_type": "section", "default_enabled": True},
{"code": "farm-ai-assistant", "name": "دستیار هوشمند مزرعه", "feature_type": "page", "default_enabled": True},
{"code": "pest-detection", "name": "تشخیص آفات گیاهی", "feature_type": "page", "default_enabled": True},
{"code": "sensor-page", "name": "صفحه سنسور", "feature_type": "page", "default_enabled": True},
{"code": "greenhouse-dashboard", "name": "Greenhouse Dashboard", "feature_type": "page", "default_enabled": True},
]
@@ -64,6 +65,7 @@ DEFAULT_ACCESS_RULES = [
"priority": 20,
"features": ["sensor-page"],
"sensor_catalogs": [SENSOR_7_NAME],
"metadata": {"sensor_catalog_names": [SENSOR_7_NAME]},
"sensor_catalog_codes": [SENSOR_7_CODE],
"metadata": {"sensor_catalog_codes": [SENSOR_7_CODE]},
},
]
@@ -0,0 +1,20 @@
from django.db import migrations
def enable_default_feature_access(apps, schema_editor):
AccessFeature = apps.get_model("access_control", "AccessFeature")
from access_control.catalog import DEFAULT_ACCESS_FEATURES
default_enabled_codes = [item["code"] for item in DEFAULT_ACCESS_FEATURES if item.get("default_enabled", False)]
AccessFeature.objects.filter(code__in=default_enabled_codes).update(default_enabled=True)
class Migration(migrations.Migration):
dependencies = [
("access_control", "0003_seed_default_access_rules"),
]
operations = [
migrations.RunPython(enable_default_feature_access, migrations.RunPython.noop),
]
@@ -0,0 +1,26 @@
from django.db import migrations
def backfill_farm_subscription_plans(apps, schema_editor):
SubscriptionPlan = apps.get_model("access_control", "SubscriptionPlan")
FarmHub = apps.get_model("farm_hub", "FarmHub")
default_plan = (
SubscriptionPlan.objects.filter(is_active=True, metadata__is_default=True).order_by("name").first()
or SubscriptionPlan.objects.filter(code="gold", is_active=True).first()
)
if default_plan is None:
return
FarmHub.objects.filter(subscription_plan__isnull=True).update(subscription_plan=default_plan)
class Migration(migrations.Migration):
dependencies = [
("access_control", "0004_enable_default_feature_access"),
("farm_hub", "0007_farmhub_subscription_plan"),
]
operations = [
migrations.RunPython(backfill_farm_subscription_plans, migrations.RunPython.noop),
]
-3
View File
@@ -12,8 +12,6 @@ class SubscriptionPlanSerializer(serializers.ModelSerializer):
class FarmAccessProfileSerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField()
subscription_plan = SubscriptionPlanSerializer(allow_null=True)
features = serializers.DictField()
groups = serializers.DictField()
matched_rules = serializers.ListField()
resolved_from_profile = serializers.BooleanField()
@@ -31,4 +29,3 @@ class FarmAccessProfileCacheSerializer(serializers.ModelSerializer):
"created_at",
"updated_at",
]
+37 -6
View File
@@ -1,19 +1,31 @@
from django.utils import timezone
from .models import AccessFeature, AccessRule, FarmAccessProfile
from .catalog import GOLD_PLAN_CODE
from .models import AccessFeature, AccessRule, FarmAccessProfile, SubscriptionPlan
def _manager_id_set(manager):
return {obj.id for obj in manager.all()}
def get_effective_subscription_plan(farm):
if getattr(farm, "subscription_plan_id", None):
return farm.subscription_plan
return (
SubscriptionPlan.objects.filter(is_active=True, metadata__is_default=True).order_by("name").first()
or SubscriptionPlan.objects.filter(code=GOLD_PLAN_CODE, is_active=True).first()
)
def rule_matches_farm(rule, farm, product_ids=None, sensor_catalog_ids=None):
if not rule.is_active:
return False
subscription_plan_ids = _manager_id_set(rule.subscription_plans)
if subscription_plan_ids:
if farm.subscription_plan_id is None or farm.subscription_plan_id not in subscription_plan_ids:
subscription_plan = get_effective_subscription_plan(farm)
if subscription_plan is None or subscription_plan.id not in subscription_plan_ids:
return False
farm_type_ids = _manager_id_set(rule.farm_types)
@@ -36,6 +48,14 @@ def rule_matches_farm(rule, farm, product_ids=None, sensor_catalog_ids=None):
if not sensor_catalog_ids or sensor_catalog_rule_ids.isdisjoint(sensor_catalog_ids):
return False
sensor_catalog_rule_codes = set(rule.metadata.get("sensor_catalog_codes", [])) if isinstance(rule.metadata, dict) else set()
if sensor_catalog_rule_codes:
farm_sensor_catalog_codes = set(
farm.sensors.exclude(sensor_catalog__code__isnull=True).values_list("sensor_catalog__code", flat=True)
)
if not farm_sensor_catalog_codes or sensor_catalog_rule_codes.isdisjoint(farm_sensor_catalog_codes):
return False
sensor_catalog_rule_names = set(rule.metadata.get("sensor_catalog_names", [])) if isinstance(rule.metadata, dict) else set()
if sensor_catalog_rule_names:
farm_sensor_catalog_names = set(
@@ -48,6 +68,7 @@ def rule_matches_farm(rule, farm, product_ids=None, sensor_catalog_ids=None):
def build_farm_access_profile(farm):
subscription_plan = get_effective_subscription_plan(farm)
features = AccessFeature.objects.all().order_by("feature_type", "code")
resolved = {
feature.code: {
@@ -112,11 +133,11 @@ def build_farm_access_profile(farm):
return {
"farm_uuid": str(farm.farm_uuid),
"subscription_plan": {
"uuid": str(farm.subscription_plan.uuid),
"code": farm.subscription_plan.code,
"name": farm.subscription_plan.name,
"uuid": str(subscription_plan.uuid),
"code": subscription_plan.code,
"name": subscription_plan.name,
}
if farm.subscription_plan_id
if subscription_plan is not None
else None,
"features": profile.cached_features,
"groups": profile.cached_groups,
@@ -125,6 +146,16 @@ def build_farm_access_profile(farm):
}
def build_farm_access_profile_response(farm):
profile_data = build_farm_access_profile(farm)
return {
"farm_uuid": profile_data["farm_uuid"],
"subscription_plan": profile_data["subscription_plan"],
"matched_rules": profile_data["matched_rules"],
"resolved_from_profile": profile_data["resolved_from_profile"],
}
def is_feature_enabled_for_farm(farm, feature_code):
profile = getattr(farm, "access_profile", None)
if profile and isinstance(profile.cached_features, dict):
+2 -3
View File
@@ -9,7 +9,7 @@ from farm_hub.models import FarmHub
from .models import SubscriptionPlan
from .serializers import FarmAccessProfileSerializer, SubscriptionPlanSerializer
from .services import build_farm_access_profile
from .services import build_farm_access_profile_response
class AccessControlBaseView(APIView):
@@ -52,6 +52,5 @@ class FarmAccessProfileView(AccessControlBaseView):
if farm is None:
return Response({"code": 404, "msg": "Farm not found."}, status=status.HTTP_404_NOT_FOUND)
data = build_farm_access_profile(farm)
data = build_farm_access_profile_response(farm)
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)