Updated project with latest changes

This commit is contained in:
Stalin
2026-05-13 00:50:32 +05:30
parent 10d9baab76
commit e33ace8056
13 changed files with 1032 additions and 261 deletions
+87 -12
View File
@@ -202,8 +202,24 @@ def admin_dashboard():
"""Get admin dashboard statistics"""
try:
total_courses = db.courses.count_documents({})
total_lessons = db.lessons.count_documents({})
total_modules = db.modules.count_documents({})
# ✅ FIXED: Count modules and lessons from embedded course structure
total_modules = 0
total_lessons = 0
courses = list(db.courses.find({}, {"modules": 1}))
for course in courses:
modules = course.get("modules", [])
total_modules += len(modules)
for module in modules:
lessons = module.get("lessons", [])
total_lessons += len(lessons)
# Fallback to separate collections if they exist
if total_modules == 0:
total_modules = db.modules.count_documents({})
if total_lessons == 0:
total_lessons = db.lessons.count_documents({})
total_users = db.users.count_documents({})
total_logs = db.security_logs.count_documents({})
active_students = db.users.count_documents({
@@ -1159,14 +1175,47 @@ def get_admin_courses():
"""Get all courses for admin management"""
try:
print("Fetching courses from database...")
courses = list(db.courses.find({}, {"_id": 0}))
courses = list(db.courses.find({}))
print(f"Found {len(courses)} courses")
def normalize_title(value):
return " ".join(str(value or "").lower().split())
def course_score(item):
score = 0
for field in ["subject", "difficulty", "mentor", "description", "video_url", "embed_url"]:
if item.get(field):
score += 1
if isinstance(item.get("modules"), list):
score += min(len(item.get("modules", [])), 5)
return score
for course in courses:
if not course.get("id") and "_id" in course:
course["id"] = str(course["_id"])
if "_id" in course:
del course["_id"]
if not course.get("title"):
course["title"] = course.get("name", "")
if not course.get("subject"):
course["subject"] = course.get("category", course.get("topic", ""))
if not course.get("difficulty"):
course["difficulty"] = course.get("level", "")
if not course.get("mentor"):
course["mentor"] = course.get("instructor", course.get("instructor_name", course.get("mentor_name", "")))
if not course.get("description"):
course["description"] = course.get("summary", "")
course["students"] = course.get("students", 0)
course["status"] = "published"
return jsonify(courses)
deduped = {}
for course in courses:
key = normalize_title(course.get("title")) or course.get("id")
existing = deduped.get(key)
if not existing or course_score(course) > course_score(existing):
deduped[key] = course
return jsonify(list(deduped.values()))
except Exception as e:
print(f"Error fetching courses: {str(e)}")
return jsonify({"error": str(e)}), 500
@@ -1261,16 +1310,27 @@ def delete_course(course_id):
try:
print(f"Deleting course: {course_id}")
course = db.courses.find_one({"id": course_id})
if not course:
try:
course = db.courses.find_one({"_id": ObjectId(course_id)})
except Exception:
course = None
if not course:
return jsonify({"error": "Course not found"}), 404
course_key = course.get("id") or str(course.get("_id"))
# Delete related lessons first
lesson_result = db.lessons.delete_many({"course_id": course_id})
lesson_result = db.lessons.delete_many({"course_id": course_key})
print(f"Deleted {lesson_result.deleted_count} related lessons")
# Delete related modules
module_result = db.modules.delete_many({"course_id": course_id})
module_result = db.modules.delete_many({"course_id": course_key})
print(f"Deleted {module_result.deleted_count} related modules")
# Delete the course
result = db.courses.delete_one({"id": course_id})
result = db.courses.delete_one({"_id": course.get("_id")})
if result.deleted_count == 0:
return jsonify({"error": "Course not found"}), 404
@@ -1690,8 +1750,23 @@ def get_admin_stats():
"""Get detailed admin statistics"""
try:
total_courses = db.courses.count_documents({})
total_lessons = db.lessons.count_documents({})
total_modules = db.modules.count_documents({})
# ✅ FIXED: Count modules and lessons from embedded course structure
total_modules = 0
total_lessons = 0
courses = list(db.courses.find({}, {"modules": 1}))
for course in courses:
modules = course.get("modules", [])
total_modules += len(modules)
for module in modules:
lessons = module.get("lessons", [])
total_lessons += len(lessons)
# Fallback to separate collections if they exist
if total_modules == 0:
total_modules = db.modules.count_documents({})
if total_lessons == 0:
total_lessons = db.lessons.count_documents({})
# Course statistics by subject
pipeline = [
+1 -1
View File
@@ -18,7 +18,7 @@ client = MongoClient(mongo_uri)
db = client.openlearnx
# JWT secret - must be set via environment variable
JWT_SECRET = os.getenv('JWT_SECRET')
JWT_SECRET = os.getenv('JWT_SECRET_KEY') or os.getenv('JWT_SECRET')
if not JWT_SECRET:
import warnings
import tempfile
+13 -2
View File
@@ -807,9 +807,20 @@ def get_user_certificates(user_id):
db = create_isolated_mongodb_connection()
if db is None:
return jsonify({"error": "Database connection failed"}), 500
normalized_user_id = str(user_id).strip()
if normalized_user_id.startswith("0x"):
normalized_user_id = normalized_user_id.lower()
certificates = list(db.certificates.find(
{"user_id": user_id},
{
"$or": [
{"user_id": user_id},
{"user_id": normalized_user_id},
{"wallet_address": user_id},
{"wallet_address": normalized_user_id},
]
},
{"_id": 0, "encrypted_wallet_id": 0}
).sort("created_at", -1))
+195 -18
View File
@@ -1,5 +1,6 @@
from flask import Blueprint, jsonify, current_app, request
from pymongo import MongoClient
from bson import ObjectId
import os
from datetime import datetime
from activity_logger import log_user_activity, resolve_user_identity
@@ -16,19 +17,29 @@ db = client.openlearnx
def list_courses():
"""Get all courses - DYNAMIC from database"""
try:
courses = list(db.courses.find({}, {"_id": 0}))
courses = list(db.courses.find({}))
course_list = []
for course in courses:
# Handle both old format (id field) and new format (_id field)
course_id = course.get("id") or str(course.get("_id", ""))
modules_count = len(course.get("modules", []))
if modules_count == 0 and course_id:
modules_count = db.modules.count_documents({"course_id": course_id})
course_data = {
"id": course.get("id"),
"title": course.get("title"),
"subject": course.get("subject"),
"description": course.get("description"),
"difficulty": course.get("difficulty"),
"mentor": course.get("mentor"),
"video_url": course.get("video_url"),
"embed_url": course.get("embed_url"),
"id": course_id,
"title": course.get("title", ""),
"subject": course.get("subject", ""),
"description": course.get("description", ""),
"difficulty": course.get("difficulty", ""),
"mentor": course.get("mentor", course.get("instructor", "")),
"video_url": course.get("video_url", ""),
"embed_url": course.get("embed_url", ""),
"thumbnail": course.get("thumbnail", ""),
"instructor": course.get("instructor", ""),
"duration_hours": course.get("duration_hours", 0),
"level": course.get("level", ""),
"modules": modules_count,
"progress": course.get("progress", 0)
}
course_list.append(course_data)
@@ -42,26 +53,111 @@ def list_courses():
def get_course(course_id):
"""Get specific course details - DYNAMIC"""
try:
course = db.courses.find_one({"id": course_id}, {"_id": 0})
# Try multiple ways to find the course
course = None
# First try as string _id (new UUID format)
course = db.courses.find_one({"_id": course_id})
# If not found, try as ObjectId (old format)
if not course:
try:
course = db.courses.find_one({"_id": ObjectId(course_id)})
except:
pass
# If still not found, try as 'id' field (legacy)
if not course:
course = db.courses.find_one({"id": course_id})
if not course:
return jsonify({"error": "Course not found"}), 404
# Convert ObjectId to string for JSON serialization
course_id_value = course.get("id") or str(course.get("_id"))
course["id"] = course_id_value
course["_id"] = str(course.get("_id"))
if not course.get("mentor"):
course["mentor"] = course.get("instructor", "")
return jsonify(course)
except Exception as e:
print(f"Error in get_course: {e}")
return jsonify({"error": "Failed to fetch course"}), 500
@bp.route("/<course_id>/modules", methods=["GET"])
def get_course_modules(course_id):
"""Get modules for a course from modules collection or embedded structure."""
try:
modules = list(db.modules.find({"course_id": course_id}).sort("order", 1))
if not modules:
course = db.courses.find_one({"id": course_id})
if not course:
try:
course = db.courses.find_one({"_id": ObjectId(course_id)})
except Exception:
course = None
if course:
embedded = course.get("modules", [])
for module in embedded:
module["course_id"] = course_id
return jsonify({"success": True, "modules": embedded})
for module in modules:
if "_id" in module:
module["id"] = str(module["_id"])
del module["_id"]
return jsonify({"success": True, "modules": modules})
except Exception as e:
print(f"Error fetching modules: {e}")
return jsonify({"error": "Failed to fetch modules"}), 500
@bp.route("/modules/<module_id>/lessons", methods=["GET"])
def get_public_module_lessons(module_id):
"""Get lessons for a module for public course pages."""
try:
lessons = list(db.lessons.find({"module_id": module_id}).sort("order", 1))
for lesson in lessons:
if "_id" in lesson:
lesson["id"] = str(lesson["_id"])
del lesson["_id"]
return jsonify({"success": True, "lessons": lessons})
except Exception as e:
print(f"Error fetching lessons: {e}")
return jsonify({"error": "Failed to fetch lessons"}), 500
@bp.route("/<course_id>/lessons/<lesson_id>", methods=["GET"])
def get_lesson(course_id, lesson_id):
"""Get specific lesson content - DYNAMIC"""
try:
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"_id": 0})
# Find course with either format
course = None
course = db.courses.find_one({"_id": course_id})
if not course:
try:
course = db.courses.find_one({"_id": ObjectId(course_id)})
except:
pass
if not course:
course = db.courses.find_one({"id": course_id})
if not lesson:
return jsonify({"error": "Lesson not found"}), 404
if not course:
return jsonify({"error": "Course not found"}), 404
return jsonify(lesson)
# Search for lesson in embedded modules structure
for module in course.get("modules", []):
for lesson in module.get("lessons", []):
if lesson.get("lesson_id") == lesson_id or lesson.get("id") == lesson_id:
return jsonify(lesson)
# Fallback: check lessons collection
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id})
if lesson:
lesson["_id"] = str(lesson.get("_id", ""))
return jsonify(lesson)
return jsonify({"error": "Lesson not found"}), 404
except Exception as e:
print(f"Error in get_lesson: {e}")
return jsonify({"error": "Failed to fetch lesson"}), 500
@@ -74,9 +170,31 @@ def mark_lesson_complete(course_id, lesson_id):
user_id = identity.get("user_id")
if user_id:
course = db.courses.find_one({"id": course_id}, {"title": 1}) or {}
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"title": 1}) or {}
# Find course with new or old format
course = db.courses.find_one({"_id": course_id}, {"title": 1})
if not course:
try:
course = db.courses.find_one({"_id": ObjectId(course_id)}, {"title": 1})
except:
pass
if not course:
course = db.courses.find_one({"id": course_id}, {"title": 1})
course = course or {}
# Find lesson title from embedded structure or lessons collection
lesson_title = lesson_id
course_full = None
if course:
course_full = db.courses.find_one({"_id": course.get("_id")})
if course_full:
for module in course_full.get("modules", []):
for lesson in module.get("lessons", []):
if lesson.get("lesson_id") == lesson_id or lesson.get("id") == lesson_id:
lesson_title = lesson.get("title", lesson_id)
break
db.user_courses.update_one(
{"user_id": user_id, "course_id": course_id},
{
@@ -97,7 +215,7 @@ def mark_lesson_complete(course_id, lesson_id):
user_id,
"course",
"Lesson completed",
f"Completed lesson '{lesson.get('title', lesson_id)}' in course '{course.get('title', course_id)}'",
f"Completed lesson '{lesson_title}' in course '{course.get('title', course_id)}'",
{"course_id": course_id, "lesson_id": lesson_id},
points_earned=10,
)
@@ -170,6 +288,65 @@ def log_course_activity(course_id):
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/<course_id>/register", methods=["POST"])
def register_course(course_id):
"""Register/enroll a user in a course and log a dashboard notification."""
try:
identity = resolve_user_identity(request, db)
user_id = identity.get("user_id")
if not user_id:
return jsonify({"success": False, "error": "Authentication required"}), 401
course = db.courses.find_one({"id": course_id}, {"title": 1})
if not course:
try:
course = db.courses.find_one({"_id": ObjectId(course_id)}, {"title": 1})
except Exception:
course = None
course_title = course.get("title") if course else course_id
db.user_courses.update_one(
{"user_id": user_id, "course_id": course_id},
{
"$set": {
"user_id": user_id,
"course_id": course_id,
"enrolled": True,
"enrolled_at": datetime.utcnow(),
"last_activity_at": datetime.utcnow(),
},
"$setOnInsert": {"completed": False, "lessons_completed": []},
},
upsert=True,
)
log_user_activity(
db,
user_id,
"course",
"Course registered",
f"Registered for course '{course_title}'",
{"course_id": course_id},
)
return jsonify({"success": True})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/<course_id>/registration", methods=["GET"])
def get_course_registration(course_id):
"""Check whether the current user is registered for a course."""
try:
identity = resolve_user_identity(request, db)
user_id = identity.get("user_id")
if not user_id:
return jsonify({"success": False, "registered": False, "error": "Authentication required"}), 401
record = db.user_courses.find_one({"user_id": user_id, "course_id": course_id})
return jsonify({"success": True, "registered": bool(record)})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/<course_id>/progress", methods=["GET"])
def get_course_progress(course_id):
"""Get user's progress in a specific course"""
+66 -27
View File
@@ -28,15 +28,31 @@ def verify_wallet_authentication():
# ✅ FIXED: Verify JWT signature using JWT_SECRET_KEY
from flask import current_app
jwt_secret = current_app.config.get('JWT_SECRET_KEY') or os.getenv('JWT_SECRET_KEY')
fallback_secret = os.getenv('JWT_SECRET')
decoded = None
if jwt_secret:
decoded = jwt.decode(
token,
jwt_secret,
algorithms=["HS256", "RS256"]
)
else:
logger.error("JWT_SECRET_KEY not configured")
decoded = None
try:
decoded = jwt.decode(
token,
jwt_secret,
algorithms=["HS256", "RS256"],
)
except Exception as e:
logger.warning(f"⚠️ JWT decode failed with JWT_SECRET_KEY: {e}")
if decoded is None and fallback_secret:
try:
decoded = jwt.decode(
token,
fallback_secret,
algorithms=["HS256", "RS256"],
)
except Exception as e:
logger.warning(f"⚠️ JWT decode failed with JWT_SECRET: {e}")
if decoded is None:
logger.error("JWT secrets not configured or token invalid")
if decoded:
user_id = decoded.get('sub') or decoded.get('user_id') or decoded.get('uid') or decoded.get('wallet_address')
@@ -140,13 +156,33 @@ def get_comprehensive_stats():
"wallet_address": wallet_address
})
identity_candidates = {str(user_id)}
if wallet_address:
identity_candidates.add(str(wallet_address).lower())
# Resolve user identity aliases to avoid missing data across auth methods.
user_doc = None
try:
maybe_oid = ObjectId(str(user_id))
user_doc = db.users.find_one({"_id": maybe_oid})
except Exception:
user_doc = db.users.find_one({"wallet_address": str(user_id).lower()}) or db.users.find_one({"email": str(user_id).lower()})
if user_doc:
if user_doc.get("_id"):
identity_candidates.add(str(user_doc.get("_id")))
if user_doc.get("wallet_address"):
identity_candidates.add(str(user_doc.get("wallet_address")).lower())
if user_doc.get("email"):
identity_candidates.add(str(user_doc.get("email")).lower())
# ✅ FETCH ONLY REAL DATA FROM MONGODB
user_stats = db.user_stats.find_one({"user_id": user_id})
courses = list(db.user_courses.find({"user_id": user_id}))
quizzes = list(db.user_quizzes.find({"user_id": user_id}))
coding_submissions = list(db.user_submissions.find({"user_id": user_id}))
blockchain_data = db.user_blockchain.find_one({"user_id": user_id})
achievements = list(db.user_achievements.find({"user_id": user_id}))
user_stats = db.user_stats.find_one({"user_id": {"$in": list(identity_candidates)}})
courses = list(db.user_courses.find({"user_id": {"$in": list(identity_candidates)}}))
quizzes = list(db.user_quizzes.find({"user_id": {"$in": list(identity_candidates)}}))
coding_submissions = list(db.user_submissions.find({"user_id": {"$in": list(identity_candidates)}}))
blockchain_data = db.user_blockchain.find_one({"user_id": {"$in": list(identity_candidates)}})
achievements = list(db.user_achievements.find({"user_id": {"$in": list(identity_candidates)}}))
# Convert ObjectIds to strings for JSON serialization
for collection in [courses, quizzes, coding_submissions, achievements]:
@@ -185,7 +221,8 @@ def get_comprehensive_stats():
# Real calculations (no fake data)
total_xp = calculate_real_total_xp(courses, quizzes, coding_submissions, achievements)
courses_completed = len([c for c in courses if c.get('completed', False)])
completed_courses = len([c for c in courses if c.get('completed', False)])
courses_completed = len(courses) if courses else completed_courses
coding_problems_solved = len(coding_submissions)
quiz_accuracy = calculate_real_quiz_accuracy(quizzes)
coding_streak = calculate_real_coding_streak(coding_submissions)
@@ -197,6 +234,7 @@ def get_comprehensive_stats():
comprehensive_stats = {
"total_xp": total_xp,
"courses_completed": courses_completed,
"completed_courses": completed_courses,
"coding_problems_solved": coding_problems_solved,
"quiz_accuracy": quiz_accuracy,
"streak_data": {
@@ -206,7 +244,7 @@ def get_comprehensive_stats():
},
"total_courses": len(courses),
"total_quizzes": len(quizzes),
"global_rank": calculate_real_global_rank(user_stats, user_id) if user_stats else 0,
"global_rank": calculate_real_global_rank(user_stats, user_id, total_xp),
"weekly_activity": weekly_activity,
"monthly_goals": {
"target": user_stats.get('monthly_target', 0) if user_stats else 0,
@@ -766,10 +804,10 @@ def get_empty_stats(wallet_address=None):
def calculate_real_total_xp(courses, quizzes, submissions, achievements):
"""Calculate total XP from ONLY real MongoDB data"""
course_xp = sum([c.get('points', 0) for c in courses if c.get('completed', False)])
quiz_xp = sum([q.get('points', 0) for q in quizzes])
coding_xp = sum([s.get('points_earned', 0) for s in submissions])
achievement_xp = sum([a.get('points', 0) for a in achievements])
course_xp = sum([c.get('points', 0) or 0 for c in courses if c.get('completed', False)])
quiz_xp = sum([q.get('points', q.get('score', 0) or 0) or 0 for q in quizzes])
coding_xp = sum([s.get('points_earned', s.get('score', 0) or 0) or 0 for s in submissions])
achievement_xp = sum([a.get('points', 0) or 0 for a in achievements])
total = course_xp + quiz_xp + coding_xp + achievement_xp
logger.info(f"📊 Real XP calculation: courses={course_xp}, quizzes={quiz_xp}, coding={coding_xp}, achievements={achievement_xp}, total={total}")
@@ -853,16 +891,17 @@ def calculate_real_quiz_accuracy(quizzes):
logger.info(f"📊 Real quiz accuracy: {accuracy}% from {len(quizzes)} quizzes")
return accuracy
def calculate_real_global_rank(user_stats, user_id):
def calculate_real_global_rank(user_stats, user_id, total_xp=None):
"""Calculate global rank from ONLY real MongoDB data"""
if not user_stats:
return 0
user_xp = user_stats.get('total_xp', 0)
user_xp = None
if user_stats:
user_xp = user_stats.get('total_xp', 0)
if user_xp is None:
user_xp = total_xp or 0
try:
higher_ranked = db.user_stats.count_documents({"total_xp": {"$gt": user_xp}})
rank = higher_ranked + 1
rank = higher_ranked + 1 if user_xp > 0 else 0
logger.info(f"📊 Real global rank: {rank} (XP: {user_xp})")
return rank
except Exception as e:
+32
View File
@@ -0,0 +1,32 @@
from flask import Blueprint, jsonify
from pymongo import MongoClient
from bson import ObjectId
import os
bp = Blueprint("modules_public", __name__)
mongo_uri = os.getenv("MONGODB_URI", "mongodb://localhost:27017/")
client = MongoClient(mongo_uri)
db = client.openlearnx
@bp.route("/<module_id>/lessons", methods=["GET"])
def get_public_module_lessons(module_id):
"""Public: get lessons for a module by module id."""
try:
lessons = list(db.lessons.find({"module_id": module_id}).sort("order", 1))
if not lessons:
try:
oid = ObjectId(module_id)
lessons = list(db.lessons.find({"module_id": oid}).sort("order", 1))
except Exception:
lessons = []
for lesson in lessons:
if "_id" in lesson:
lesson["id"] = str(lesson["_id"])
del lesson["_id"]
return jsonify({"success": True, "lessons": lessons})
except Exception as e:
return jsonify({"error": f"Failed to fetch lessons: {str(e)}"}), 500