Coverage for app/services/user_service.py: 59%
148 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-02 02:49 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-02 02:49 +0000
1from app.controllers.database import client, get_service_client
2from app.models.user import User, Class
3from app.models.errors import UserAlreadyExistsError, DatabaseError
4from datetime import datetime
5from pprint import pprint
8def update_user_class_status(user_id: str, new_status: str, user_class_id=None):
9 """
10 Updates the status of a user in a specific class.
12 Args:
13 user_id (str): The student's user ID
14 new_status (str): The new status to set
15 user_class_id (str): The class ID to update status in
17 Raises:
18 DatabaseError: If update fails
19 """
20 try:
21 if user_class_id:
22 result = (
23 client.table("class_users")
24 .update({"user_class_status": new_status})
25 .match({"student_id": user_id, "class_id": user_class_id})
26 .execute()
27 )
29 if not result.data:
30 raise DatabaseError("No matching class user record found")
31 else:
32 result = (
33 client.table("users")
34 .update({"status": new_status})
35 .eq("id", user_id)
36 .execute()
37 )
39 if not result.data:
40 raise DatabaseError(f"No user record found for id {user_id}")
42 except Exception as e:
43 print(f"Error updating class user status: {e}")
44 raise DatabaseError(f"Failed to update class user status: {str(e)}")
47def get_user_class_status(user_id: str, class_id: str):
48 """
49 Retrieves the class status for a user.
51 Args:
52 user_id (str): The unique identifier of the user.
53 class_id (str, optional): The class identifier to filter by.
55 Returns:
56 dict: A dictionary containing the user's class status.
58 Raises:
59 Exception: If there is an error during the database query.
60 """
61 try:
62 query = (
63 client.table("class_users")
64 .select("user_class_status")
65 .eq("student_id", user_id)
66 .eq("class_id", class_id)
67 )
69 response = query.execute()
71 return response.data[0] if response.data else None
73 except Exception as e:
74 print(f"Error fetching user class status: {e}")
75 raise DatabaseError(f"Failed to retrieve user class status: {str(e)}")
78def create_user_section(user_id: str, class_id: str = None):
79 """
80 Creates a new user section for the given user, optionally associated with a class.
82 Args:
83 user_id (str): The unique identifier of the user.
84 class_id (str, optional): The class identifier to associate with the section.
86 Returns:
87 str: The ID of the created user section.
89 Raises:
90 DatabaseError: If there is an error during the database operation.
91 """
92 try:
93 section_data = {
94 "user_id": user_id,
95 "started_at": datetime.now().isoformat(),
96 "status": "ACTIVE",
97 }
99 if class_id:
100 section_data["class_id"] = class_id
102 result = client.table("user_sections").insert(section_data).execute()
104 if not result.data:
105 raise DatabaseError("Insert operation returned no data")
107 return result.data[0]["section_id"]
108 except Exception as e:
109 print(f"Error creating user section: {e}")
110 raise DatabaseError(f"Failed to create user section: {str(e)}")
113def get_user_section(user_id: str, class_id: str = None):
114 """
115 Retrieves or creates a user section, optionally filtered by class_id.
117 Args:
118 user_id (str): The user's unique identifier
119 class_id (str, optional): The class identifier to filter by
121 Returns:
122 str: The ID of the user section
124 Raises:
125 Exception: If database operations fail
126 """
127 try:
128 query = (
129 client.table("user_sections")
130 .select("section_id")
131 .eq("user_id", user_id)
132 .eq("status", "ACTIVE")
133 )
135 if class_id is not None:
136 query = query.eq("class_id", class_id)
137 else:
138 query = query.is_("class_id", "null")
140 active_section = query.execute()
141 active_section_data = active_section.data
143 if active_section_data:
144 return active_section_data[0]["section_id"]
145 else:
146 return create_user_section(user_id, class_id)
148 except Exception as e:
149 print(f"Error retrieving or creating user section: {e}")
150 raise DatabaseError(f"Failed to retrieve or create user section: {str(e)}")
153def update_user_section(status: str, user_section_id):
154 """
155 Updates the active user section's status. If status is COMPLETE, it also creates a new section.
157 Args:
158 user_id (str): The unique identifier of the user.
159 status (str): The new status to update to (COMPLETE or NEED_REVIEW).
161 Returns:
162 dict: New section ID if created, else just a message.
163 """
164 try:
165 client.table("user_sections").update(
166 {"status": status, "ended_at": datetime.now().isoformat()}
167 ).eq("section_id", user_section_id).execute()
169 except Exception as e:
170 print(f"Error completing and creating user section: {e}")
171 raise DatabaseError(f"Failed to complete and create user section: {str(e)}")
174def get_classes_by_user_id(user_id: str):
175 """
176 Fetch all classes that a specific user is enrolled in.
178 Args:
179 user_id (str): The unique identifier of the user.
181 Returns:
182 list: A list of Class objects.
183 []: Empty list if the user is not enrolled in any class.
184 Raises:
185 Exception: If there is an error during the database query.
186 """
187 try:
188 response = (
189 client.table("class_users")
190 .select("user_class_status, classes(*)")
191 .eq("student_id", user_id)
192 .eq("enrollment_status", "ENROLLED")
193 .execute()
194 )
196 if not response.data:
197 print(f"No classes found for user {user_id}")
198 return []
200 class_info_list = []
201 for class_data in response.data:
202 if "classes" in class_data and class_data["classes"]:
203 user_class = Class(**class_data["classes"])
204 user_class_status = class_data.get(
205 "user_class_status", "UNKNOWN"
206 ) # Default if not present
207 class_info_list.append(
208 {
209 "userClass": user_class.to_json(), # You can adjust as needed
210 "studentStatus": user_class_status,
211 }
212 )
214 return class_info_list
216 except Exception as e:
217 print(f"Error fetching classes for user {user_id}: {e}")
218 raise DatabaseError(f"Failed to retrieve user classes: {str(e)}")
221def get_all_users():
222 try:
223 # Get all users from the `users` table
224 response = client.table("users").select("*").execute()
225 users_table_data = response.data or []
227 # Get all users from Supabase Auth
228 auth_users_response = get_service_client().auth.admin.list_users()
229 auth_users = auth_users_response
231 # Create lookup tables
232 users_by_id = {user["id"]: user for user in users_table_data}
233 auth_by_id = {user.id: user for user in auth_users}
235 all_user_ids = set(users_by_id.keys()) | set(auth_by_id.keys())
236 unified_users = []
238 for user_id in all_user_ids:
239 user_table_data = users_by_id.get(user_id)
240 auth_user = auth_by_id.get(user_id)
242 entry = {
243 **(user_table_data or {}), # include users table fields if any
244 "auth_email": auth_user.email if auth_user else None,
245 "auth_created_at": auth_user.created_at if auth_user else None,
246 "providers": (
247 auth_user.app_metadata.get("providers") if auth_user else None
248 ),
249 "last_updated_at": auth_user.updated_at if auth_user else None,
250 "avatar_url": (
251 auth_user.user_metadata.get("avatar_url") if auth_user else None
252 ),
253 "last_sign_in": auth_user.last_sign_in_at if auth_user else None,
254 }
256 # Determine the source
257 if user_table_data and auth_user:
258 entry["source"] = "both"
259 elif user_table_data:
260 entry["source"] = "users_only"
261 else:
262 entry["source"] = "auth_only"
263 entry["id"] = auth_user.id # ensure ID is present
265 unified_users.append(entry)
266 return unified_users
268 except Exception as e:
269 print(f"Error fetching all users: {e}")
270 raise DatabaseError(f"Failed to retrieve all users: {str(e)}")
273def get_user_by_id(user_id: str):
274 """
275 Fetch a single user by ID.
277 Args:
278 user_id (str): The unique identifier of the user.
280 Returns:
281 dict: A dictionary containing user details if found.
282 None: If the user does not exist.
284 Raises:
285 Exception: If there is an error during the database query.
286 """
287 try:
288 response = client.table("users").select("*").eq("id", user_id).execute()
289 user_records = response.data or []
291 user_data = user_records[0] if user_records else {}
293 auth_user_response = get_service_client().auth.admin.get_user_by_id(user_id)
294 auth_user = auth_user_response.user
296 user_data["auth_email"] = auth_user.email if auth_user else None
297 user_data["auth_created_at"] = auth_user.created_at if auth_user else None
298 user_data["providers"] = (
299 auth_user.app_metadata.get("providers") if auth_user else None
300 )
301 user_data["last_updated_at"] = auth_user.updated_at if auth_user else None
302 user_data["avatar_url"] = (
303 auth_user.user_metadata.get("avatar_url") if auth_user else None
304 )
305 user_data["last_sign_in"] = auth_user.last_sign_in_at if auth_user else None
307 if user_data and auth_user:
308 user_data["source"] = "both"
309 elif user_data:
310 user_data["source"] = "users_only"
311 else:
312 user_data["source"] = "auth_only"
313 user_data["id"] = auth_user.id if auth_user else None # Ensure ID present
315 return User(**user_data)
317 except Exception as e:
318 print(f"Error fetching user {user_id}: {e}")
319 return None
322def update_user_settings(user_id, new_settings):
323 """
324 Updates the settings for a user.
326 Args:
327 user_id (str): The unique identifier of the user.
328 new_settings (dict): A dictionary containing the new settings to update.
330 Returns:
331 dict: A dictionary containing the updated user settings.
332 """
333 try:
334 client.table("users").update({"settings": new_settings}).eq(
335 "id", user_id
336 ).execute()
337 except Exception as e:
338 print(f"Error updating user settings: {e}")
339 raise DatabaseError(f"Failed to update user settings: {str(e)}")
342def delete_user(user_id: str):
343 """
344 Deletes a user by their ID.
346 Args:
347 user_id (str): The unique identifier of the user.
349 Returns:
350 dict: A dictionary containing the user ID if successful.
351 None: If the user does not exist.
353 Raises:
354 Exception: If there is an error during the database deletion.
355 """
356 try:
357 # Check if user exists first
358 existing_user = client.table("users").select("*").eq("id", user_id).execute()
360 if not existing_user.data:
361 return None
363 # Delete the user
364 client.table("users").delete().eq("id", user_id).execute()
365 get_service_client().auth.admin.delete_user(user_id)
367 except Exception as e:
368 print(f"Error deleting user: {e}")
369 raise DatabaseError(f"Failed to delete user: {str(e)}")
372def edit_user(user_id: str, user_data: dict):
373 """
374 Updates a user's information.
376 Args:
377 user_id (str): The unique identifier of the user.
378 user_data (dict): A dictionary containing the new user data to update.
380 Returns:
381 dict: A dictionary containing the updated user information.
382 None: If the user does not exist.
384 Raises:
385 Exception: If there is an error during the database update.
386 """
387 try:
389 existing_user = client.table("users").select("*").eq("id", user_id).execute()
391 if not existing_user.data:
392 return None
394 get_service_client().auth.admin.update_user_by_id(
395 user_id,
396 {
397 "email": user_data.get("email"),
398 "user_metadata": {
399 "avatar_url": user_data.get("avatar_url"),
400 "first_name": user_data.get("firstName"),
401 "last_name": user_data.get("lastName"),
402 },
403 },
404 )
405 non_admin_user_data = {
406 "first_name": user_data.get("firstName"),
407 "last_name": user_data.get("lastName"),
408 "email": user_data.get("email"),
409 "role": user_data.get("role"),
410 }
411 client.table("users").update(non_admin_user_data).eq("id", user_id).execute()
413 except Exception as e:
414 print(f"Error updating user: {e}")
415 raise DatabaseError(f"Failed to update user: {str(e)}")