zuleleee commited on
Commit
78ebe75
·
verified ·
1 Parent(s): 3952061

Upload 4 files

Browse files
mongodb_adapter.py ADDED
@@ -0,0 +1,882 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MongoDB Adapter - Replaces SQLAlchemy ORM with MongoDB operations
3
+ Provides compatibility layer for existing codebase
4
+ """
5
+
6
+ from datetime import datetime
7
+ from typing import Optional, Dict, Any, List
8
+ try:
9
+ from bson.objectid import ObjectId
10
+ except ImportError:
11
+ # Fallback if standalone bson package conflicts
12
+ from pymongo.bson.objectid import ObjectId
13
+ from mongodb_config import mongodb, get_db, serialize_document
14
+ import bcrypt
15
+ import logging
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ class MongoDBAdapter:
20
+ """Adapter to replace SQLAlchemy operations with MongoDB"""
21
+
22
+ def __init__(self):
23
+ self.db = get_db()
24
+
25
+ # ==================== Admin Operations ====================
26
+
27
+ def create_admin(self, username: str, password: str, role: str = "admin") -> Optional[str]:
28
+ """Create a new admin user with role
29
+
30
+ Roles:
31
+ - 'admin': Can upload syllabus only
32
+ - 'school_admin': Can enroll students only
33
+ """
34
+ try:
35
+ password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
36
+
37
+ admin_doc = {
38
+ "username": username,
39
+ "password_hash": password_hash,
40
+ "role": role, # 'admin' or 'school_admin'
41
+ "created_at": datetime.utcnow()
42
+ }
43
+
44
+ result = self.db.admins.insert_one(admin_doc)
45
+ logger.info(f"✅ Admin created: {username} with role: {role}")
46
+ return str(result.inserted_id)
47
+ except Exception as e:
48
+ logger.error(f"Error creating admin: {e}")
49
+ return None
50
+
51
+ def get_admin_by_username(self, username: str) -> Optional[Dict]:
52
+ """Get admin by username"""
53
+ admin = self.db.admins.find_one({"username": username})
54
+ return serialize_document(admin) if admin else None
55
+
56
+ def get_admin_role(self, username: str) -> Optional[str]:
57
+ """Get admin role by username"""
58
+ admin = self.db.admins.find_one({"username": username}, {"role": 1})
59
+ return admin.get("role", "admin") if admin else None
60
+
61
+ def verify_admin_password(self, username: str, password: str) -> bool:
62
+ """Verify admin password"""
63
+ admin = self.db.admins.find_one({"username": username})
64
+ if not admin:
65
+ return False
66
+ return bcrypt.checkpw(password.encode('utf-8'), admin["password_hash"].encode('utf-8'))
67
+
68
+ def update_admin_password(self, username: str, new_password: str, role: Optional[str] = None) -> bool:
69
+ """Update admin password and optionally role"""
70
+ try:
71
+ password_hash = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
72
+ update_data = {"password_hash": password_hash, "updated_at": datetime.utcnow()}
73
+ if role:
74
+ update_data["role"] = role
75
+ result = self.db.admins.update_one(
76
+ {"username": username},
77
+ {"$set": update_data}
78
+ )
79
+ return result.modified_count > 0
80
+ except Exception as e:
81
+ logger.error(f"Error updating admin password: {e}")
82
+ return False
83
+
84
+ def create_school_admin(self, username: str, password: str, school_id: str, name: str) -> Optional[str]:
85
+ """Create a new school_admin user with school_id association (max 50 school_admins)"""
86
+ try:
87
+ # Check if we've reached the limit of 50 school_admins
88
+ school_admin_count = self.db.admins.count_documents({"role": "school_admin"})
89
+ if school_admin_count >= 50:
90
+ logger.error(f"Maximum limit of 50 school_admins reached")
91
+ return None
92
+
93
+ password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
94
+
95
+ admin_doc = {
96
+ "username": username,
97
+ "password_hash": password_hash,
98
+ "role": "school_admin",
99
+ "school_id": school_id,
100
+ "name": name,
101
+ "created_at": datetime.utcnow()
102
+ }
103
+
104
+ result = self.db.admins.insert_one(admin_doc)
105
+ logger.info(f"✅ School admin created: {username} for school_id: {school_id}")
106
+ return str(result.inserted_id)
107
+ except Exception as e:
108
+ logger.error(f"Error creating school admin: {e}")
109
+ return None
110
+
111
+ def get_school_admins(self, school_id: Optional[str] = None) -> List[Dict]:
112
+ """Get all school_admins, optionally filtered by school_id"""
113
+ query = {"role": "school_admin"}
114
+ if school_id:
115
+ query["school_id"] = school_id
116
+ admins = self.db.admins.find(query).sort("created_at", -1)
117
+ return [serialize_document(a) for a in admins]
118
+
119
+ def get_admin_school_id(self, username: str) -> Optional[str]:
120
+ """Get school_id for a school_admin"""
121
+ admin = self.db.admins.find_one({"username": username}, {"school_id": 1})
122
+ return admin.get("school_id") if admin else None
123
+
124
+ # ==================== School Operations ====================
125
+
126
+ def create_school(self, name: str) -> Optional[str]:
127
+ """Create a new school"""
128
+ try:
129
+ # Check if school with same name already exists
130
+ existing = self.db.schools.find_one({"name": name})
131
+ if existing:
132
+ logger.warning(f"School with name '{name}' already exists")
133
+ return str(existing.get("_id"))
134
+
135
+ school_doc = {
136
+ "name": name,
137
+ "created_at": datetime.utcnow()
138
+ }
139
+
140
+ result = self.db.schools.insert_one(school_doc)
141
+ logger.info(f"✅ School created: {name}")
142
+ return str(result.inserted_id)
143
+ except Exception as e:
144
+ logger.error(f"Error creating school: {e}")
145
+ return None
146
+
147
+ def get_school_by_id(self, school_id: str) -> Optional[Dict]:
148
+ """Get school by ID"""
149
+ try:
150
+ school = self.db.schools.find_one({"_id": ObjectId(school_id)})
151
+ return serialize_document(school) if school else None
152
+ except:
153
+ return None
154
+
155
+ def get_school_by_name(self, name: str) -> Optional[Dict]:
156
+ """Get school by name"""
157
+ school = self.db.schools.find_one({"name": name})
158
+ return serialize_document(school) if school else None
159
+
160
+ def get_all_schools(self) -> List[Dict]:
161
+ """Get all schools"""
162
+ schools = self.db.schools.find().sort("created_at", -1)
163
+ return [serialize_document(s) for s in schools]
164
+
165
+ def update_school(self, school_id: str, name: str) -> bool:
166
+ """Update school information"""
167
+ try:
168
+ result = self.db.schools.update_one(
169
+ {"_id": ObjectId(school_id)},
170
+ {"$set": {"name": name, "updated_at": datetime.utcnow()}}
171
+ )
172
+ return result.modified_count > 0
173
+ except Exception as e:
174
+ logger.error(f"Error updating school: {e}")
175
+ return False
176
+
177
+ def delete_school(self, school_id: str) -> bool:
178
+ """Delete a school (only if no students or admins are associated)"""
179
+ try:
180
+ # Check if any students are associated with this school
181
+ student_count = self.db.students.count_documents({"school_id": school_id})
182
+ if student_count > 0:
183
+ logger.warning(f"Cannot delete school {school_id}: {student_count} students are associated")
184
+ return False
185
+
186
+ # Check if any school_admins are associated with this school
187
+ admin_count = self.db.admins.count_documents({"school_id": school_id, "role": "school_admin"})
188
+ if admin_count > 0:
189
+ logger.warning(f"Cannot delete school {school_id}: {admin_count} school_admins are associated")
190
+ return False
191
+
192
+ result = self.db.schools.delete_one({"_id": ObjectId(school_id)})
193
+ return result.deleted_count > 0
194
+ except Exception as e:
195
+ logger.error(f"Error deleting school: {e}")
196
+ return False
197
+
198
+ # ==================== Student Operations ====================
199
+
200
+ def create_student(self, student_data: Dict) -> Optional[str]:
201
+ """Create a new student"""
202
+ try:
203
+ password_hash = bcrypt.hashpw(student_data["password"].encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
204
+
205
+ student_doc = {
206
+ "student_id": student_data["student_id"],
207
+ "username": student_data.get("username", student_data["student_id"]),
208
+ "email": student_data["email"],
209
+ "password_hash": password_hash,
210
+ "first_name": student_data["first_name"],
211
+ "last_name": student_data["last_name"],
212
+ "grade": student_data.get("grade", 0),
213
+ "school_id": student_data.get("school_id"), # Add school_id
214
+ "school_name": student_data.get("school_name", ""), # Keep for backward compatibility
215
+ "weak_topics": student_data.get("weak_topics", []),
216
+ "google_form_results": student_data.get("google_form_results", {}),
217
+ "learning_path": student_data.get("learning_path", {}),
218
+ "progress": student_data.get("progress", 0.0),
219
+ "created_at": datetime.utcnow(),
220
+ "is_active": student_data.get("is_active", True)
221
+ }
222
+
223
+ result = self.db.students.insert_one(student_doc)
224
+ logger.info(f"✅ Student created: {student_data['student_id']}")
225
+ return str(result.inserted_id)
226
+ except Exception as e:
227
+ logger.error(f"Error creating student: {e}")
228
+ return None
229
+
230
+ def get_student_by_student_id(self, student_id: str) -> Optional[Dict]:
231
+ """Get student by student_id"""
232
+ student = self.db.students.find_one({"student_id": student_id})
233
+ return serialize_document(student) if student else None
234
+
235
+ def get_student_by_id(self, id: str) -> Optional[Dict]:
236
+ """Get student by MongoDB _id"""
237
+ try:
238
+ student = self.db.students.find_one({"_id": ObjectId(id)})
239
+ return serialize_document(student) if student else None
240
+ except:
241
+ return None
242
+
243
+ def get_student_by_username(self, username: str) -> Optional[Dict]:
244
+ """Get student by username"""
245
+ student = self.db.students.find_one({"username": username})
246
+ return serialize_document(student) if student else None
247
+
248
+ def get_student_by_email(self, email: str) -> Optional[Dict]:
249
+ """Get student by email"""
250
+ student = self.db.students.find_one({"email": email})
251
+ return serialize_document(student) if student else None
252
+
253
+ def get_all_students(self, skip: int = 0, limit: int = 100, school_id: Optional[str] = None) -> List[Dict]:
254
+ """Get all students with pagination, optionally filtered by school_id"""
255
+ query = {}
256
+ if school_id:
257
+ query["school_id"] = school_id
258
+ students = self.db.students.find(query).skip(skip).limit(limit).sort("created_at", -1)
259
+ return [serialize_document(s) for s in students]
260
+
261
+ def update_student(self, student_id: str, update_data: Dict) -> bool:
262
+ """Update student information"""
263
+ try:
264
+ result = self.db.students.update_one(
265
+ {"student_id": student_id},
266
+ {"$set": {**update_data, "updated_at": datetime.utcnow()}}
267
+ )
268
+ return result.modified_count > 0
269
+ except Exception as e:
270
+ logger.error(f"Error updating student: {e}")
271
+ return False
272
+
273
+ def update_student_by_id(self, student_mongo_id: str, update_data: Dict) -> bool:
274
+ """Update student by MongoDB _id"""
275
+ try:
276
+ result = self.db.students.update_one(
277
+ {"_id": ObjectId(student_mongo_id)},
278
+ {"$set": {**update_data, "updated_at": datetime.utcnow()}}
279
+ )
280
+ return result.modified_count > 0
281
+ except Exception as e:
282
+ logger.error(f"Error updating student by ID: {e}")
283
+ return False
284
+
285
+ def update_student_password(self, student_id: str, new_password: str) -> bool:
286
+ """Update student password"""
287
+ try:
288
+ password_hash = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
289
+ return self.update_student(student_id, {"password_hash": password_hash})
290
+ except Exception as e:
291
+ logger.error(f"Error updating student password: {e}")
292
+ return False
293
+
294
+ def delete_student(self, student_id: str) -> bool:
295
+ """Delete a student and all related data"""
296
+ try:
297
+ # Get student first to find by student_id or MongoDB _id
298
+ student = self.get_student_by_student_id(student_id)
299
+ if not student:
300
+ # Try by MongoDB _id
301
+ try:
302
+ student = self.get_student_by_id(student_id)
303
+ except:
304
+ pass
305
+
306
+ if not student:
307
+ return False
308
+
309
+ mongo_id = student.get("_id") or student_id
310
+
311
+ # Delete related data
312
+ # Delete chat messages
313
+ self.db.chat_messages.delete_many({"student_id": student.get("student_id")})
314
+
315
+ # Delete assessments
316
+ assessments = self.db.assessments.find({"student_id": student.get("student_id")})
317
+ assessment_ids = [str(a["_id"]) for a in assessments]
318
+
319
+ # Delete student answers
320
+ self.db.student_answers.delete_many({"assessment_id": {"$in": assessment_ids}})
321
+
322
+ # Delete questions
323
+ self.db.questions.delete_many({"assessment_id": {"$in": assessment_ids}})
324
+
325
+ # Delete assessments
326
+ self.db.assessments.delete_many({"student_id": student.get("student_id")})
327
+
328
+ # Remove student from syllabus assigned_students lists
329
+ self.db.syllabi.update_many(
330
+ {"assigned_students": student.get("student_id")},
331
+ {"$pull": {"assigned_students": student.get("student_id")}}
332
+ )
333
+
334
+ # Delete the student
335
+ result = self.db.students.delete_one({"_id": ObjectId(mongo_id) if isinstance(mongo_id, str) else mongo_id})
336
+ return result.deleted_count > 0
337
+ except Exception as e:
338
+ logger.error(f"Error deleting student: {e}")
339
+ return False
340
+
341
+ def get_students_by_grade(self, grade: int, school_id: Optional[str] = None) -> List[Dict]:
342
+ """Get students filtered by grade, optionally filtered by school_id"""
343
+ query = {"grade": grade}
344
+ if school_id:
345
+ query["school_id"] = school_id
346
+ students = self.db.students.find(query).sort("created_at", -1)
347
+ return [serialize_document(s) for s in students]
348
+
349
+ def update_student_learning_path(self, student_id: str, learning_path: Dict) -> bool:
350
+ """Update student's learning path"""
351
+ return self.update_student(student_id, {"learning_path": learning_path})
352
+
353
+ def update_student_progress(self, student_id: str, progress: float) -> bool:
354
+ """Update student's progress"""
355
+ return self.update_student(student_id, {"progress": progress})
356
+
357
+ def verify_student_password(self, username: str, password: str) -> bool:
358
+ """Verify student password"""
359
+ student = self.db.students.find_one({"username": username})
360
+ if not student:
361
+ return False
362
+ return bcrypt.checkpw(password.encode('utf-8'), student["password_hash"].encode('utf-8'))
363
+
364
+ # ==================== Syllabus Operations ====================
365
+
366
+ def create_syllabus(self, title: str, content: Dict, assigned_students: List[str] = None) -> Optional[str]:
367
+ """Create a new syllabus"""
368
+ try:
369
+ syllabus_doc = {
370
+ "title": title,
371
+ "content": content,
372
+ "assigned_students": assigned_students or [],
373
+ "uploaded_at": datetime.utcnow()
374
+ }
375
+
376
+ result = self.db.syllabi.insert_one(syllabus_doc)
377
+ logger.info(f"✅ Syllabus created: {title}")
378
+ return str(result.inserted_id)
379
+ except Exception as e:
380
+ logger.error(f"Error creating syllabus: {e}")
381
+ return None
382
+
383
+ def get_syllabus_by_id(self, id: str) -> Optional[Dict]:
384
+ """Get syllabus by ID"""
385
+ try:
386
+ syllabus = self.db.syllabi.find_one({"_id": ObjectId(id)})
387
+ return serialize_document(syllabus) if syllabus else None
388
+ except:
389
+ return None
390
+
391
+ def get_all_syllabi(self) -> List[Dict]:
392
+ """Get all syllabi"""
393
+ syllabi = self.db.syllabi.find().sort("uploaded_at", -1)
394
+ return [serialize_document(s) for s in syllabi]
395
+
396
+ def get_student_syllabi(self, student_id: str) -> List[Dict]:
397
+ """Get syllabi assigned to a student - checks both student_id field and MongoDB _id"""
398
+ try:
399
+ # Try exact match first
400
+ syllabi = list(self.db.syllabi.find({"assigned_students": student_id}))
401
+
402
+ # If not found, try with ObjectId conversion (in case student_id is MongoDB _id)
403
+ if not syllabi:
404
+ try:
405
+ from mongodb_config import ObjectId
406
+ if ObjectId and len(student_id) == 24: # ObjectId is 24 hex characters
407
+ syllabi = list(self.db.syllabi.find({"assigned_students": ObjectId(student_id)}))
408
+ except:
409
+ pass
410
+
411
+ # Also try string comparison (in case stored as string but we're searching with different format)
412
+ if not syllabi:
413
+ # Get all syllabi and filter in Python (less efficient but more reliable)
414
+ all_syllabi = list(self.db.syllabi.find({}))
415
+ syllabi = []
416
+ for s in all_syllabi:
417
+ assigned = s.get("assigned_students", [])
418
+ assigned_str = [str(sid) for sid in assigned]
419
+ if str(student_id) in assigned_str:
420
+ syllabi.append(s)
421
+
422
+ return [serialize_document(s) for s in syllabi]
423
+ except Exception as e:
424
+ logger.error(f"Error getting student syllabi for {student_id}: {e}")
425
+ return []
426
+
427
+ def assign_syllabus_to_students(self, syllabus_id: str, student_ids: List[str]) -> bool:
428
+ """Assign syllabus to multiple students"""
429
+ try:
430
+ result = self.db.syllabi.update_one(
431
+ {"_id": ObjectId(syllabus_id)},
432
+ {"$addToSet": {"assigned_students": {"$each": student_ids}}}
433
+ )
434
+ return result.modified_count > 0
435
+ except Exception as e:
436
+ logger.error(f"Error assigning syllabus: {e}")
437
+ return False
438
+
439
+ def update_syllabus(self, syllabus_id: str, update_data: Dict) -> bool:
440
+ """Update syllabus information"""
441
+ try:
442
+ result = self.db.syllabi.update_one(
443
+ {"_id": ObjectId(syllabus_id)},
444
+ {"$set": {**update_data, "updated_at": datetime.utcnow()}}
445
+ )
446
+ return result.modified_count > 0
447
+ except Exception as e:
448
+ logger.error(f"Error updating syllabus: {e}")
449
+ return False
450
+
451
+ def delete_syllabus(self, syllabus_id: str) -> bool:
452
+ """Delete a syllabus"""
453
+ try:
454
+ result = self.db.syllabi.delete_one({"_id": ObjectId(syllabus_id)})
455
+ return result.deleted_count > 0
456
+ except Exception as e:
457
+ logger.error(f"Error deleting syllabus: {e}")
458
+ return False
459
+
460
+ def get_syllabi_by_grade(self, grade: int) -> List[Dict]:
461
+ """Get syllabi filtered by grade (extracted from title)"""
462
+ all_syllabi = self.get_all_syllabi()
463
+ matching = []
464
+ for s in all_syllabi:
465
+ title_lower = (s.get("title") or "").lower()
466
+ if f"grade {grade}" in title_lower or f"grade{grade}" in title_lower or title_lower.endswith(f"grade {grade}"):
467
+ matching.append(s)
468
+ return matching
469
+
470
+ def remove_student_from_syllabus(self, syllabus_id: str, student_id: str) -> bool:
471
+ """Remove a student from syllabus assigned_students list"""
472
+ try:
473
+ result = self.db.syllabi.update_one(
474
+ {"_id": ObjectId(syllabus_id)},
475
+ {"$pull": {"assigned_students": student_id}}
476
+ )
477
+ return result.modified_count > 0
478
+ except Exception as e:
479
+ logger.error(f"Error removing student from syllabus: {e}")
480
+ return False
481
+
482
+ # ==================== Assessment Operations ====================
483
+
484
+ def create_assessment(self, assessment_data: Dict) -> Optional[str]:
485
+ """Create a new assessment"""
486
+ try:
487
+ assessment_doc = {
488
+ "student_id": assessment_data["student_id"],
489
+ "syllabus_id": assessment_data.get("syllabus_id"),
490
+ "attempt_number": assessment_data.get("attempt_number", 1),
491
+ "session_id": assessment_data.get("session_id", ""),
492
+ "total_questions": assessment_data.get("total_questions", 0),
493
+ "questions_asked": assessment_data.get("questions_asked", 0),
494
+ "correct_answers": assessment_data.get("correct_answers", 0),
495
+ "score_percentage": assessment_data.get("score_percentage", 0.0),
496
+ "weak_topics": assessment_data.get("weak_topics", []),
497
+ "created_at": datetime.utcnow(),
498
+ "completed_at": assessment_data.get("completed_at")
499
+ }
500
+
501
+ result = self.db.assessments.insert_one(assessment_doc)
502
+ return str(result.inserted_id)
503
+ except Exception as e:
504
+ logger.error(f"Error creating assessment: {e}")
505
+ return None
506
+
507
+ def get_assessment_by_id(self, id: str) -> Optional[Dict]:
508
+ """Get assessment by ID"""
509
+ try:
510
+ assessment = self.db.assessments.find_one({"_id": ObjectId(id)})
511
+ return serialize_document(assessment) if assessment else None
512
+ except:
513
+ return None
514
+
515
+ def get_student_assessments(self, student_id: str) -> List[Dict]:
516
+ """Get all assessments for a student"""
517
+ assessments = self.db.assessments.find({"student_id": student_id}).sort("created_at", -1)
518
+ return [serialize_document(a) for a in assessments]
519
+
520
+ def update_assessment(self, assessment_id: str, update_data: Dict) -> bool:
521
+ """Update assessment"""
522
+ try:
523
+ result = self.db.assessments.update_one(
524
+ {"_id": ObjectId(assessment_id)},
525
+ {"$set": update_data}
526
+ )
527
+ return result.modified_count > 0
528
+ except Exception as e:
529
+ logger.error(f"Error updating assessment: {e}")
530
+ return False
531
+
532
+ # ==================== Question Operations ====================
533
+
534
+ def create_question(self, question_data: Dict) -> Optional[str]:
535
+ """Create a new question"""
536
+ try:
537
+ question_doc = {
538
+ "assessment_id": question_data["assessment_id"],
539
+ "question_text": question_data["question_text"],
540
+ "option_a": question_data["option_a"],
541
+ "option_b": question_data["option_b"],
542
+ "option_c": question_data["option_c"],
543
+ "option_d": question_data["option_d"],
544
+ "correct_answer": question_data["correct_answer"],
545
+ "topic": question_data["topic"],
546
+ "difficulty": question_data.get("difficulty", "medium"),
547
+ "created_at": datetime.utcnow()
548
+ }
549
+
550
+ result = self.db.questions.insert_one(question_doc)
551
+ return str(result.inserted_id)
552
+ except Exception as e:
553
+ logger.error(f"Error creating question: {e}")
554
+ return None
555
+
556
+ def get_questions_by_assessment(self, assessment_id: str) -> List[Dict]:
557
+ """Get all questions for an assessment"""
558
+ questions = self.db.questions.find({"assessment_id": assessment_id})
559
+ return [serialize_document(q) for q in questions]
560
+
561
+ # ==================== Student Answer Operations ====================
562
+
563
+ def save_student_answer(self, answer_data: Dict) -> Optional[str]:
564
+ """Save student's answer"""
565
+ try:
566
+ answer_doc = {
567
+ "assessment_id": answer_data["assessment_id"],
568
+ "question_id": answer_data["question_id"],
569
+ "student_id": answer_data["student_id"],
570
+ "selected_answer": answer_data["selected_answer"],
571
+ "is_correct": answer_data["is_correct"],
572
+ "created_at": datetime.utcnow()
573
+ }
574
+
575
+ result = self.db.student_answers.insert_one(answer_doc)
576
+ return str(result.inserted_id)
577
+ except Exception as e:
578
+ logger.error(f"Error saving answer: {e}")
579
+ return None
580
+
581
+ def get_student_answers(self, assessment_id: str, student_id: str) -> List[Dict]:
582
+ """Get all answers for a student's assessment"""
583
+ answers = self.db.student_answers.find({
584
+ "assessment_id": assessment_id,
585
+ "student_id": student_id
586
+ })
587
+ return [serialize_document(a) for a in answers]
588
+
589
+ # ==================== Chat Operations ====================
590
+
591
+ def save_chat_message(self, message_data: Dict) -> Optional[str]:
592
+ """Save a chat message"""
593
+ try:
594
+ message_doc = {
595
+ "student_id": message_data["student_id"],
596
+ "session_id": message_data["session_id"],
597
+ "role": message_data["role"],
598
+ "content": message_data["content"],
599
+ "created_at": datetime.utcnow()
600
+ }
601
+
602
+ result = self.db.chat_messages.insert_one(message_doc)
603
+ return str(result.inserted_id)
604
+ except Exception as e:
605
+ logger.error(f"Error saving chat message: {e}")
606
+ return None
607
+
608
+ def get_chat_history(self, student_id: str, session_id: str, limit: int = 50) -> List[Dict]:
609
+ """Get chat history for a session"""
610
+ messages = self.db.chat_messages.find({
611
+ "student_id": student_id,
612
+ "session_id": session_id
613
+ }).sort("created_at", 1).limit(limit)
614
+ return [serialize_document(m) for m in messages]
615
+
616
+ # ==================== Learning Path Progress Operations ====================
617
+
618
+ def save_learning_progress(self, progress_data: Dict) -> Optional[str]:
619
+ """Save learning progress"""
620
+ try:
621
+ progress_doc = {
622
+ "student_id": progress_data["student_id"],
623
+ "module_id": progress_data.get("module_id"),
624
+ "lesson_id": progress_data.get("lesson_id"),
625
+ "practice_id": progress_data.get("practice_id"),
626
+ "quiz_id": progress_data.get("quiz_id"),
627
+ "item_type": progress_data["item_type"],
628
+ "is_completed": progress_data.get("is_completed", False),
629
+ "score": progress_data.get("score"),
630
+ "completed_at": progress_data.get("completed_at"),
631
+ "created_at": datetime.utcnow()
632
+ }
633
+
634
+ result = self.db.learning_path_progress.insert_one(progress_doc)
635
+ return str(result.inserted_id)
636
+ except Exception as e:
637
+ logger.error(f"Error saving learning progress: {e}")
638
+ return None
639
+
640
+ def get_student_progress(self, student_id: str) -> List[Dict]:
641
+ """Get all progress for a student"""
642
+ progress = self.db.learning_path_progress.find({"student_id": student_id})
643
+ return [serialize_document(p) for p in progress]
644
+
645
+ def update_progress_item(self, student_id: str, item_id: int, item_type: str,
646
+ is_completed: bool, score: float = None) -> bool:
647
+ """Update a specific progress item"""
648
+ try:
649
+ query = {"student_id": student_id, "item_type": item_type}
650
+
651
+ if item_type == "lesson":
652
+ query["lesson_id"] = item_id
653
+ elif item_type == "practice":
654
+ query["practice_id"] = item_id
655
+ elif item_type in ["mini_quiz", "module_quiz"]:
656
+ query["quiz_id"] = item_id
657
+
658
+ update_data = {
659
+ "is_completed": is_completed,
660
+ "completed_at": datetime.utcnow() if is_completed else None
661
+ }
662
+
663
+ if score is not None:
664
+ update_data["score"] = score
665
+
666
+ result = self.db.learning_path_progress.update_one(query, {"$set": update_data}, upsert=True)
667
+ return result.modified_count > 0 or result.upserted_id is not None
668
+ except Exception as e:
669
+ logger.error(f"Error updating progress: {e}")
670
+ return False
671
+
672
+ # ==================== Practice Assessment Operations ====================
673
+
674
+ def create_practice_assessment(self, practice_data: Dict) -> Optional[str]:
675
+ """Create practice assessment"""
676
+ try:
677
+ practice_doc = {
678
+ "student_id": practice_data["student_id"],
679
+ "topic": practice_data["topic"],
680
+ "questions": practice_data["questions"],
681
+ "answers": practice_data.get("answers"),
682
+ "score": practice_data.get("score"),
683
+ "completed_at": practice_data.get("completed_at"),
684
+ "created_at": datetime.utcnow()
685
+ }
686
+
687
+ result = self.db.practice_assessments.insert_one(practice_doc)
688
+ return str(result.inserted_id)
689
+ except Exception as e:
690
+ logger.error(f"Error creating practice assessment: {e}")
691
+ return None
692
+
693
+ def get_practice_assessment(self, practice_id: str) -> Optional[Dict]:
694
+ """Get practice assessment by ID"""
695
+ try:
696
+ practice = self.db.practice_assessments.find_one({"_id": ObjectId(practice_id)})
697
+ return serialize_document(practice) if practice else None
698
+ except:
699
+ return None
700
+
701
+ def update_practice_assessment(self, practice_id: str, update_data: Dict) -> bool:
702
+ """Update practice assessment"""
703
+ try:
704
+ result = self.db.practice_assessments.update_one(
705
+ {"_id": ObjectId(practice_id)},
706
+ {"$set": update_data}
707
+ )
708
+ return result.modified_count > 0
709
+ except Exception as e:
710
+ logger.error(f"Error updating practice assessment: {e}")
711
+ return False
712
+
713
+ # ==================== Microsoft Forms Operations ====================
714
+
715
+ def create_microsoft_form(self, form_data: Dict) -> Optional[str]:
716
+ """Create a new Microsoft Form"""
717
+ try:
718
+ form_doc = {
719
+ "title": form_data["title"],
720
+ "description": form_data.get("description", ""),
721
+ "form_url": form_data["form_url"],
722
+ "form_id": form_data.get("form_id"),
723
+ "subject": form_data["subject"],
724
+ "grade": form_data["grade"],
725
+ "school_id": form_data.get("school_id"),
726
+ "is_active": form_data.get("is_active", True),
727
+ "created_by": form_data["created_by"],
728
+ "created_at": datetime.utcnow(),
729
+ "due_date": form_data.get("due_date")
730
+ }
731
+
732
+ result = self.db.microsoft_forms.insert_one(form_doc)
733
+ logger.info(f"✅ Microsoft Form created: {form_data['title']}")
734
+ return str(result.inserted_id)
735
+ except Exception as e:
736
+ logger.error(f"Error creating Microsoft Form: {e}")
737
+ return None
738
+
739
+ def get_microsoft_form_by_id(self, form_id: str) -> Optional[Dict]:
740
+ """Get Microsoft Form by ID"""
741
+ try:
742
+ # Try ObjectId first
743
+ form = self.db.microsoft_forms.find_one({"_id": ObjectId(form_id)})
744
+ if form:
745
+ return serialize_document(form)
746
+ # If not found, try as string ID
747
+ form = self.db.microsoft_forms.find_one({"id": form_id})
748
+ return serialize_document(form) if form else None
749
+ except Exception as e:
750
+ logger.warning(f"Error getting Microsoft Form by ID {form_id}: {e}")
751
+ return None
752
+
753
+ def get_microsoft_forms(self, subject: Optional[str] = None, grade: Optional[int] = None) -> List[Dict]:
754
+ """Get all Microsoft Forms, optionally filtered"""
755
+ query = {"is_active": True}
756
+ if subject:
757
+ query["subject"] = subject
758
+ if grade:
759
+ query["grade"] = grade
760
+
761
+ forms = self.db.microsoft_forms.find(query).sort("created_at", -1)
762
+ return [serialize_document(f) for f in forms]
763
+
764
+ def create_microsoft_form_submission(self, submission_data: Dict) -> Optional[str]:
765
+ """Create a new Microsoft Form submission"""
766
+ try:
767
+ submission_doc = {
768
+ "student_id": str(submission_data.get("student_id", "")), # Store as string for consistency
769
+ "form_id": str(submission_data.get("form_id", "")), # Store as string for consistency
770
+ "submission_url": submission_data.get("submission_url", ""),
771
+ "responses": submission_data.get("responses", {}),
772
+ "subject": submission_data["subject"],
773
+ "score": submission_data.get("score"),
774
+ "weak_points": submission_data.get("weak_points", []),
775
+ "submitted_at": datetime.utcnow(),
776
+ "processed": submission_data.get("processed", False)
777
+ }
778
+
779
+ result = self.db.microsoft_form_submissions.insert_one(submission_doc)
780
+ logger.info(f"✅ Microsoft Form submission created: {result.inserted_id}")
781
+ return str(result.inserted_id)
782
+ except Exception as e:
783
+ logger.error(f"Error creating Microsoft Form submission: {e}", exc_info=True)
784
+ return None
785
+
786
+ def get_microsoft_form_submission(self, submission_id: str) -> Optional[Dict]:
787
+ """Get Microsoft Form submission by ID"""
788
+ try:
789
+ submission = self.db.microsoft_form_submissions.find_one({"_id": ObjectId(submission_id)})
790
+ return serialize_document(submission) if submission else None
791
+ except Exception as e:
792
+ logger.warning(f"Error getting Microsoft Form submission {submission_id}: {e}")
793
+ return None
794
+
795
+ def get_student_microsoft_form_submissions(self, student_id: str) -> List[Dict]:
796
+ """Get all Microsoft Form submissions for a student"""
797
+ # Try to find by student_id (string) or by integer ID
798
+ try:
799
+ submissions = self.db.microsoft_form_submissions.find({
800
+ "$or": [
801
+ {"student_id": student_id},
802
+ {"student_id": int(student_id) if student_id.isdigit() else None}
803
+ ]
804
+ }).sort("submitted_at", -1)
805
+ return [serialize_document(s) for s in submissions]
806
+ except:
807
+ # Fallback: just search by string
808
+ submissions = self.db.microsoft_form_submissions.find({"student_id": str(student_id)}).sort("submitted_at", -1)
809
+ return [serialize_document(s) for s in submissions]
810
+
811
+ def get_form_submissions(self, form_id: str) -> List[Dict]:
812
+ """Get all submissions for a form"""
813
+ # Try to find by form_id (string) or by ObjectId
814
+ try:
815
+ submissions = self.db.microsoft_form_submissions.find({
816
+ "$or": [
817
+ {"form_id": form_id},
818
+ {"form_id": ObjectId(form_id)}
819
+ ]
820
+ }).sort("submitted_at", -1)
821
+ return [serialize_document(s) for s in submissions]
822
+ except:
823
+ # Fallback: just search by string
824
+ submissions = self.db.microsoft_form_submissions.find({"form_id": str(form_id)}).sort("submitted_at", -1)
825
+ return [serialize_document(s) for s in submissions]
826
+
827
+ def update_microsoft_form_submission(self, submission_id: str, update_data: Dict) -> bool:
828
+ """Update Microsoft Form submission"""
829
+ try:
830
+ result = self.db.microsoft_form_submissions.update_one(
831
+ {"_id": ObjectId(submission_id)},
832
+ {"$set": update_data}
833
+ )
834
+ if result.modified_count > 0:
835
+ logger.info(f"✅ Updated Microsoft Form submission {submission_id}")
836
+ return result.modified_count > 0
837
+ except Exception as e:
838
+ logger.error(f"Error updating Microsoft Form submission {submission_id}: {e}", exc_info=True)
839
+ return False
840
+
841
+ def delete_microsoft_form(self, form_id: str) -> bool:
842
+ """Delete a Microsoft Form and all related submissions"""
843
+ try:
844
+ # First, try to find the form
845
+ form = self.get_microsoft_form_by_id(form_id)
846
+ if not form:
847
+ logger.warning(f"Microsoft Form {form_id} not found for deletion")
848
+ return False
849
+
850
+ # Delete all submissions for this form
851
+ form_id_str = str(form.get("_id") or form.get("id", form_id))
852
+ try:
853
+ submissions_deleted = self.db.microsoft_form_submissions.delete_many({
854
+ "$or": [
855
+ {"form_id": form_id_str},
856
+ {"form_id": ObjectId(form_id_str)}
857
+ ]
858
+ })
859
+ logger.info(f"Deleted {submissions_deleted.deleted_count} submissions for form {form_id_str}")
860
+ except Exception as e:
861
+ logger.warning(f"Error deleting submissions for form {form_id_str}: {e}")
862
+
863
+ # Delete the form itself
864
+ try:
865
+ result = self.db.microsoft_forms.delete_one({"_id": ObjectId(form_id_str)})
866
+ if result.deleted_count > 0:
867
+ logger.info(f"✅ Deleted Microsoft Form {form_id_str}")
868
+ return True
869
+ else:
870
+ # Try by form_id field if ObjectId didn't work
871
+ result = self.db.microsoft_forms.delete_one({"form_id": form_id_str})
872
+ return result.deleted_count > 0
873
+ except Exception as e:
874
+ # If ObjectId conversion fails, try as string
875
+ result = self.db.microsoft_forms.delete_one({"id": form_id_str})
876
+ return result.deleted_count > 0
877
+ except Exception as e:
878
+ logger.error(f"Error deleting Microsoft Form {form_id}: {e}", exc_info=True)
879
+ return False
880
+
881
+ # Global adapter instance
882
+ db_adapter = MongoDBAdapter()
mongodb_config.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MongoDB Configuration and Connection Manager
3
+ Replaces SQLite with MongoDB for the Adaptive Learning Platform
4
+ """
5
+
6
+ import sys
7
+ import logging
8
+ import os
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Check for bson import conflict before importing pymongo
13
+ try:
14
+ import bson
15
+ # Check if this is the standalone bson package (which doesn't have SON)
16
+ if not hasattr(bson, 'SON') and not hasattr(bson, 'objectid'):
17
+ logger.error("""
18
+ ⚠️ BSON Import Conflict Detected!
19
+
20
+ A standalone 'bson' package is installed that conflicts with pymongo.
21
+ This will cause import errors.
22
+
23
+ To fix this, run:
24
+ pip uninstall bson
25
+ pip install --upgrade pymongo
26
+
27
+ Or see: BSON_CONFLICT_FIX.md for detailed instructions.
28
+ """)
29
+ # Don't exit - let pymongo fail with a clearer error
30
+ except ImportError:
31
+ pass # bson not installed, which is fine
32
+
33
+ from pymongo import MongoClient, ASCENDING, DESCENDING
34
+ from pymongo.errors import ConnectionFailure, OperationFailure
35
+ try:
36
+ from bson.objectid import ObjectId
37
+ except ImportError:
38
+ # Fallback if standalone bson package conflicts
39
+ try:
40
+ from pymongo.bson.objectid import ObjectId
41
+ except ImportError:
42
+ # Last resort - try direct import
43
+ import pymongo
44
+ ObjectId = pymongo.bson.objectid.ObjectId
45
+
46
+ from datetime import datetime
47
+ from typing import Optional, Dict, Any, List
48
+
49
+ # MongoDB Configuration
50
+ MONGODB_CONNECTION_STRING = "mongodb+srv://raziullah0316_db_user:8GXp76aJwsg2i6Rn@learning.tlwwzix.mongodb.net/"
51
+ MONGODB_DATABASE_NAME = "learning"
52
+
53
+ class MongoDBManager:
54
+ """MongoDB Connection and Operations Manager"""
55
+
56
+ def __init__(self):
57
+ self.client: Optional[MongoClient] = None
58
+ self.db = None
59
+ self._connected = False
60
+
61
+ def connect(self) -> bool:
62
+ """Connect to MongoDB"""
63
+ try:
64
+ self.client = MongoClient(
65
+ MONGODB_CONNECTION_STRING,
66
+ serverSelectionTimeoutMS=5000,
67
+ connectTimeoutMS=10000,
68
+ retryWrites=True,
69
+ w='majority'
70
+ )
71
+
72
+ # Test connection
73
+ self.client.admin.command('ping')
74
+ self.db = self.client[MONGODB_DATABASE_NAME]
75
+ self._connected = True
76
+
77
+ logger.info(f"✅ Connected to MongoDB: {MONGODB_DATABASE_NAME}")
78
+ self._create_indexes()
79
+ return True
80
+
81
+ except ConnectionFailure as e:
82
+ logger.error(f"❌ Failed to connect to MongoDB: {e}")
83
+ self._connected = False
84
+ return False
85
+ except Exception as e:
86
+ logger.error(f"❌ Unexpected error connecting to MongoDB: {e}")
87
+ self._connected = False
88
+ return False
89
+
90
+ def _create_indexes(self):
91
+ """Create indexes for better performance"""
92
+ try:
93
+ # Admins collection
94
+ self.db.admins.create_index([("username", ASCENDING)], unique=True)
95
+
96
+ # Students collection
97
+ self.db.students.create_index([("student_id", ASCENDING)], unique=True)
98
+ self.db.students.create_index([("email", ASCENDING)])
99
+
100
+ # Syllabi collection
101
+ self.db.syllabi.create_index([("title", ASCENDING)])
102
+ self.db.syllabi.create_index([("uploaded_at", DESCENDING)])
103
+
104
+ # Assessments collection
105
+ self.db.assessments.create_index([("student_id", ASCENDING)])
106
+ self.db.assessments.create_index([("syllabus_id", ASCENDING)])
107
+ self.db.assessments.create_index([("created_at", DESCENDING)])
108
+
109
+ # MCQ Questions collection
110
+ self.db.mcq_questions.create_index([("syllabus_id", ASCENDING)])
111
+ self.db.mcq_questions.create_index([("topic", ASCENDING)])
112
+
113
+ # Student Answers collection
114
+ self.db.student_answers.create_index([("student_id", ASCENDING), ("assessment_id", ASCENDING)])
115
+
116
+ # Learning Paths collection
117
+ self.db.learning_paths.create_index([("student_id", ASCENDING)])
118
+ self.db.learning_paths.create_index([("status", ASCENDING)])
119
+
120
+ # Chat Sessions collection
121
+ self.db.chat_sessions.create_index([("student_id", ASCENDING)])
122
+ self.db.chat_sessions.create_index([("created_at", DESCENDING)])
123
+
124
+ # Learning Progress collection
125
+ self.db.learning_progress.create_index([("student_id", ASCENDING)])
126
+ self.db.learning_progress.create_index([("learning_path_id", ASCENDING)])
127
+
128
+ # Microsoft Forms collection
129
+ self.db.microsoft_forms.create_index([("subject", ASCENDING)])
130
+ self.db.microsoft_forms.create_index([("grade", ASCENDING)])
131
+ self.db.microsoft_forms.create_index([("is_active", ASCENDING)])
132
+ self.db.microsoft_forms.create_index([("created_at", DESCENDING)])
133
+
134
+ # Microsoft Form Submissions collection
135
+ self.db.microsoft_form_submissions.create_index([("student_id", ASCENDING)])
136
+ self.db.microsoft_form_submissions.create_index([("form_id", ASCENDING)])
137
+ self.db.microsoft_form_submissions.create_index([("subject", ASCENDING)])
138
+ self.db.microsoft_form_submissions.create_index([("submitted_at", DESCENDING)])
139
+
140
+ logger.info("✅ MongoDB indexes created successfully")
141
+ except Exception as e:
142
+ logger.warning(f"⚠️ Failed to create some indexes: {e}")
143
+
144
+ def is_connected(self) -> bool:
145
+ """Check if connected to MongoDB"""
146
+ return self._connected and self.client is not None
147
+
148
+ def close(self):
149
+ """Close MongoDB connection"""
150
+ if self.client:
151
+ self.client.close()
152
+ self._connected = False
153
+ logger.info("MongoDB connection closed")
154
+
155
+ def serialize_doc(self, doc: Optional[Dict]) -> Optional[Dict]:
156
+ """Convert MongoDB document to JSON-serializable format"""
157
+ if doc is None:
158
+ return None
159
+
160
+ doc = dict(doc) # Make a copy
161
+
162
+ if "_id" in doc:
163
+ doc["id"] = str(doc["_id"])
164
+ del doc["_id"]
165
+
166
+ for key, value in doc.items():
167
+ if isinstance(value, ObjectId):
168
+ doc[key] = str(value)
169
+ elif isinstance(value, datetime):
170
+ doc[key] = value.isoformat()
171
+ elif isinstance(value, dict):
172
+ doc[key] = self.serialize_doc(value)
173
+ elif isinstance(value, list):
174
+ doc[key] = [self.serialize_doc(item) if isinstance(item, dict) else item for item in value]
175
+
176
+ return doc
177
+
178
+ def get_object_id(self, id_str: str) -> ObjectId:
179
+ """Convert string to ObjectId"""
180
+ try:
181
+ return ObjectId(id_str)
182
+ except:
183
+ return None
184
+
185
+ # Global MongoDB instance
186
+ mongodb = MongoDBManager()
187
+
188
+ # Helper functions for backward compatibility with SQLite code
189
+ def get_db():
190
+ """Get MongoDB database instance (replaces SQLAlchemy session)"""
191
+ if not mongodb.is_connected():
192
+ mongodb.connect()
193
+ return mongodb.db
194
+
195
+ def serialize_document(doc: Dict) -> Dict:
196
+ """Serialize MongoDB document"""
197
+ return mongodb.serialize_doc(doc)
mongodb_integration.py ADDED
File without changes
mongodb_models.py ADDED
File without changes