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

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 

6 

7 

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. 

11 

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 

16 

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 ) 

28 

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 ) 

38 

39 if not result.data: 

40 raise DatabaseError(f"No user record found for id {user_id}") 

41 

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)}") 

45 

46 

47def get_user_class_status(user_id: str, class_id: str): 

48 """ 

49 Retrieves the class status for a user. 

50 

51 Args: 

52 user_id (str): The unique identifier of the user. 

53 class_id (str, optional): The class identifier to filter by. 

54 

55 Returns: 

56 dict: A dictionary containing the user's class status. 

57 

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 ) 

68 

69 response = query.execute() 

70 

71 return response.data[0] if response.data else None 

72 

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)}") 

76 

77 

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. 

81 

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. 

85 

86 Returns: 

87 str: The ID of the created user section. 

88 

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 } 

98 

99 if class_id: 

100 section_data["class_id"] = class_id 

101 

102 result = client.table("user_sections").insert(section_data).execute() 

103 

104 if not result.data: 

105 raise DatabaseError("Insert operation returned no data") 

106 

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)}") 

111 

112 

113def get_user_section(user_id: str, class_id: str = None): 

114 """ 

115 Retrieves or creates a user section, optionally filtered by class_id. 

116 

117 Args: 

118 user_id (str): The user's unique identifier 

119 class_id (str, optional): The class identifier to filter by 

120 

121 Returns: 

122 str: The ID of the user section 

123 

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 ) 

134 

135 if class_id is not None: 

136 query = query.eq("class_id", class_id) 

137 else: 

138 query = query.is_("class_id", "null") 

139 

140 active_section = query.execute() 

141 active_section_data = active_section.data 

142 

143 if active_section_data: 

144 return active_section_data[0]["section_id"] 

145 else: 

146 return create_user_section(user_id, class_id) 

147 

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)}") 

151 

152 

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. 

156 

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). 

160 

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() 

168 

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)}") 

172 

173 

174def get_classes_by_user_id(user_id: str): 

175 """ 

176 Fetch all classes that a specific user is enrolled in. 

177 

178 Args: 

179 user_id (str): The unique identifier of the user. 

180 

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 ) 

195 

196 if not response.data: 

197 print(f"No classes found for user {user_id}") 

198 return [] 

199 

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 ) 

213 

214 return class_info_list 

215 

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)}") 

219 

220 

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 [] 

226 

227 # Get all users from Supabase Auth 

228 auth_users_response = get_service_client().auth.admin.list_users() 

229 auth_users = auth_users_response 

230 

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} 

234 

235 all_user_ids = set(users_by_id.keys()) | set(auth_by_id.keys()) 

236 unified_users = [] 

237 

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) 

241 

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 } 

255 

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 

264 

265 unified_users.append(entry) 

266 return unified_users 

267 

268 except Exception as e: 

269 print(f"Error fetching all users: {e}") 

270 raise DatabaseError(f"Failed to retrieve all users: {str(e)}") 

271 

272 

273def get_user_by_id(user_id: str): 

274 """ 

275 Fetch a single user by ID. 

276 

277 Args: 

278 user_id (str): The unique identifier of the user. 

279 

280 Returns: 

281 dict: A dictionary containing user details if found. 

282 None: If the user does not exist. 

283 

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 [] 

290 

291 user_data = user_records[0] if user_records else {} 

292 

293 auth_user_response = get_service_client().auth.admin.get_user_by_id(user_id) 

294 auth_user = auth_user_response.user 

295 

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 

306 

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 

314 

315 return User(**user_data) 

316 

317 except Exception as e: 

318 print(f"Error fetching user {user_id}: {e}") 

319 return None 

320 

321 

322def update_user_settings(user_id, new_settings): 

323 """ 

324 Updates the settings for a user. 

325 

326 Args: 

327 user_id (str): The unique identifier of the user. 

328 new_settings (dict): A dictionary containing the new settings to update. 

329 

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)}") 

340 

341 

342def delete_user(user_id: str): 

343 """ 

344 Deletes a user by their ID. 

345 

346 Args: 

347 user_id (str): The unique identifier of the user. 

348 

349 Returns: 

350 dict: A dictionary containing the user ID if successful. 

351 None: If the user does not exist. 

352 

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() 

359 

360 if not existing_user.data: 

361 return None 

362 

363 # Delete the user 

364 client.table("users").delete().eq("id", user_id).execute() 

365 get_service_client().auth.admin.delete_user(user_id) 

366 

367 except Exception as e: 

368 print(f"Error deleting user: {e}") 

369 raise DatabaseError(f"Failed to delete user: {str(e)}") 

370 

371 

372def edit_user(user_id: str, user_data: dict): 

373 """ 

374 Updates a user's information. 

375 

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. 

379 

380 Returns: 

381 dict: A dictionary containing the updated user information. 

382 None: If the user does not exist. 

383 

384 Raises: 

385 Exception: If there is an error during the database update. 

386 """ 

387 try: 

388 

389 existing_user = client.table("users").select("*").eq("id", user_id).execute() 

390 

391 if not existing_user.data: 

392 return None 

393 

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() 

412 

413 except Exception as e: 

414 print(f"Error updating user: {e}") 

415 raise DatabaseError(f"Failed to update user: {str(e)}")