Coverage for server / services / growthbook / user_scope.py: 100%
64 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"""
2Resolve teacher/student user documents and school → district chain.
4Schools and districts live in admin_staff_mongodb; users live in teacher_student_db.
5When duplicate profiles share auth0_user_id or email, prefer the one whose
6school_id resolves in the admin database.
7"""
9from typing import Any, Optional
11from beanie import PydanticObjectId
13from server.connection.database import staff_admin_db
14from server.models.clients import School
15from server.models.users import User
16from server.services.growthbook.admin_db import get_feature_db
19async def fetch_district_id_for_school(school_oid: PydanticObjectId) -> Optional[str]:
20 """Load school from admin_staff_mongodb first, then teacher_student_db fallback."""
21 if staff_admin_db is not None:
22 school_doc = await staff_admin_db["school_collection"].find_one({"_id": school_oid})
23 if school_doc and school_doc.get("district_id"):
24 return str(school_doc["district_id"])
26 school_doc = await get_feature_db()["school_collection"].find_one({"_id": school_oid})
27 if school_doc and school_doc.get("district_id"):
28 return str(school_doc["district_id"])
30 school = await School.get(school_oid)
31 if school and school.district_id:
32 return str(school.district_id)
34 return None
37async def _district_for_user_doc(user_doc: dict[str, Any]) -> Optional[str]:
38 school_id_raw = user_doc.get("school_id")
39 if not school_id_raw:
40 return None
41 try:
42 school_oid = (
43 school_id_raw
44 if isinstance(school_id_raw, PydanticObjectId)
45 else PydanticObjectId(str(school_id_raw))
46 )
47 except Exception:
48 return None
49 return await fetch_district_id_for_school(school_oid)
52async def find_best_user_document(
53 *,
54 mongodb_id: Optional[str] = None,
55 auth0_user_id: Optional[str] = None,
56 email: Optional[str] = None,
57) -> Optional[dict[str, Any]]:
58 """
59 Return the best matching user document for feature scope.
61 When multiple profiles match (duplicate auth0/email), prefer the document
62 whose school_id resolves to a district in admin_staff_mongodb.
63 """
64 collection = User.get_pymongo_collection()
65 candidates: list[dict[str, Any]] = []
66 seen_ids: set[str] = set()
68 def _add(doc: Optional[dict[str, Any]]) -> None:
69 if not doc:
70 return
71 doc_id = str(doc["_id"])
72 if doc_id in seen_ids:
73 return
74 seen_ids.add(doc_id)
75 candidates.append(doc)
77 if mongodb_id:
78 try:
79 _add(await collection.find_one({"_id": PydanticObjectId(str(mongodb_id))}))
80 except Exception:
81 pass
83 if auth0_user_id:
84 async for doc in collection.find({"auth0_user_id": auth0_user_id}):
85 _add(doc)
87 if email:
88 async for doc in collection.find({"email": email}):
89 _add(doc)
91 if not candidates:
92 return None
94 provisioned: list[tuple[dict[str, Any], str]] = []
95 for doc in candidates:
96 district_id = await _district_for_user_doc(doc)
97 if district_id:
98 provisioned.append((doc, district_id))
100 if provisioned:
101 # Prefer explicit mongodb_id match when it has a valid chain
102 if mongodb_id:
103 for doc, _ in provisioned:
104 if str(doc["_id"]) == str(mongodb_id):
105 return doc
106 return provisioned[0][0]
108 return candidates[0]