Coverage for server / services / growthbook / global_assignment_gate.py: 100%
24 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
1"""
2Global assignment access checks for tiered practice globals (feature_key gating).
3"""
5from typing import Any
7from fastapi import HTTPException, status
9from server.services.growthbook.context import TargetingContext
10from server.services.growthbook.evaluation import feature_evaluation_service
11from server.services.growthbook.registry import min_plan_for_feature, resolve_feature_key
13# Global assignments created by staff/admin/super-admin that are NOT part of the
14# tiered exam catalog are gated behind this feature (Intermediate/Premium).
15ERUDITION_TESTS_LIBRARY_FEATURE = "teacher.erudition_tests_library"
18async def _erudition_tests_library_meta(ctx: TargetingContext) -> dict[str, Any]:
19 enabled = await feature_evaluation_service.is_enabled(
20 ERUDITION_TESTS_LIBRARY_FEATURE, ctx
21 )
22 return {
23 "feature_key": ERUDITION_TESTS_LIBRARY_FEATURE,
24 "enabled": enabled,
25 "locked": not enabled,
26 "min_plan": min_plan_for_feature(ERUDITION_TESTS_LIBRARY_FEATURE),
27 }
30async def global_assignment_access_meta(
31 assignment: dict[str, Any],
32 ctx: TargetingContext,
33) -> dict[str, Any]:
34 """Return feature gating metadata for a global assignment row."""
35 feature_key = resolve_feature_key(assignment.get("feature_key"))
36 assignment_type = assignment.get("type")
37 format_value = assignment.get("format")
39 if feature_key:
40 enabled = await feature_evaluation_service.is_enabled(feature_key, ctx)
41 return {
42 "feature_key": feature_key,
43 "enabled": enabled,
44 "locked": not enabled,
45 "min_plan": min_plan_for_feature(feature_key),
46 }
48 # Staff-created globals without an exam-catalog feature_key — including
49 # STAAR/TSI/SAT/ACT + practice rows that are not tiered exam slots.
50 return await _erudition_tests_library_meta(ctx)
53async def enforce_global_assignment_access(
54 assignment: dict[str, Any],
55 ctx: TargetingContext,
56) -> None:
57 """Raise 403 when the teacher's district plan cannot access this global assignment."""
58 meta = await global_assignment_access_meta(assignment, ctx)
59 if meta["locked"] or not meta["enabled"]:
60 detail = "This global assignment is not available for your district plan."
61 if meta.get("feature_key"):
62 detail = (
63 f"Feature '{meta['feature_key']}' is not enabled for your district plan."
64 )
65 raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=detail)