Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- mongodb_adapter.py +882 -0
- mongodb_config.py +197 -0
- mongodb_integration.py +0 -0
- mongodb_models.py +0 -0
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
|