from flask import Blueprint, request, jsonify, session import uuid import random import string from datetime import datetime, timedelta from pymongo import MongoClient import os from activity_logger import log_user_activity, resolve_user_identity bp = Blueprint('exam', __name__) # MongoDB connection mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') client = MongoClient(mongo_uri) db = client.openlearnx def get_db(): """Get database connection""" return db def generate_exam_code(): """Generate a unique 6-character exam code""" while True: code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) if not db.exams.find_one({"exam_code": code}): return code @bp.route("/create-exam", methods=["POST", "OPTIONS"]) def create_exam(): """Create a new coding exam""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response try: print(f"Received create-exam request") data = request.json print(f"Request data: {data}") if not data: print("❌ No data provided") return jsonify({"error": "No data provided"}), 400 # Check for basic required fields if not data.get('title'): print("❌ Missing title") return jsonify({"error": "Missing required field: title"}), 400 if not data.get('host_name'): print("❌ Missing host_name") return jsonify({"error": "Missing required field: host_name"}), 400 # Handle different problem data formats problem_title = data.get('problem_title') or data.get('title') or 'Coding Challenge' problem_description = data.get('problem_description') or f"Solve the {problem_title} problem" # Handle problem_id if provided if data.get('problem_id'): problem_title = problem_title or data.get('problem_id').replace('-', ' ').title() print(f"Using problem_id: {data.get('problem_id')}") exam_code = generate_exam_code() exam = { "exam_code": exam_code, "title": data.get('title'), "host_name": data.get('host_name'), "created_at": datetime.now(), "status": "waiting", "duration_minutes": data.get('duration_minutes', 30), "max_participants": data.get('max_participants', 50), "problems": [{ # Changed to problems array to support multiple problems "id": "problem_1", "title": problem_title, "description": problem_description, "function_name": data.get('function_name', 'solve'), "languages": data.get('languages', ['python']), "test_cases": data.get('test_cases', [ { "input": "hello world", "expected_output": "Hello World", "description": "Basic capitalization test", "points": 10 } ]), "starter_code": data.get('starter_code', { 'python': 'def solve(input_string):\n # Write your solution here\n return input_string.title()', 'java': 'public String solve(String inputString) {\n // Write your solution here\n return inputString;\n}', 'javascript': 'function solve(inputString) {\n // Write your solution here\n return inputString;\n}' }), "constraints": data.get('constraints', ['Input will be a string', 'Length between 1-1000 characters']), "examples": data.get('examples', [ { "input": "hello world", "expected_output": "Hello World", "description": "Capitalize each word" } ]), "total_points": data.get('total_points', 100) }], # Keep backward compatibility "problem": { "title": problem_title, "description": problem_description, "function_name": data.get('function_name', 'solve'), "languages": data.get('languages', ['python']), "test_cases": data.get('test_cases', [ { "input": "hello world", "expected_output": "Hello World", "description": "Basic capitalization test" } ]), "starter_code": data.get('starter_code', { 'python': 'def solve(input_string):\n # Write your solution here\n return input_string.title()', 'java': 'public String solve(String inputString) {\n // Write your solution here\n return inputString;\n}', 'javascript': 'function solve(inputString) {\n // Write your solution here\n return inputString;\n}' }), "constraints": data.get('constraints', ['Input will be a string', 'Length between 1-1000 characters']), "examples": data.get('examples', [ { "input": "hello world", "expected_output": "Hello World", "description": "Capitalize each word" } ]) }, "participants": [], "leaderboard": [], "start_time": None, "end_time": None } print(f"✅ Creating exam with code: {exam_code}") print(f"✅ Problem title: {problem_title}") # Insert into database result = db.exams.insert_one(exam) print(f"✅ Exam created successfully with ID: {result.inserted_id}") return jsonify({ "success": True, "exam_code": exam_code, "exam_id": str(result.inserted_id), "message": f"Exam created successfully! Share code: {exam_code}", "exam_details": { "title": exam['title'], "problem_title": problem_title, "duration": exam['duration_minutes'], "max_participants": exam['max_participants'], "languages": exam['problem']['languages'] } }) except Exception as e: print(f"❌ Error creating exam: {str(e)}") import traceback traceback.print_exc() return jsonify({"error": f"Failed to create exam: {str(e)}"}), 500 @bp.route("/join-exam", methods=["POST", "OPTIONS"]) def join_exam(): """Student joins exam using unique code and their name""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response try: # Debug logging for the request print(f"🔍 Raw request data: {request.data}") print(f"🔍 Content-Type: {request.headers.get('Content-Type')}") data = request.json print(f"🔍 Parsed JSON data: {data}") if not data: print("❌ No JSON data received") return jsonify({"error": "No data provided"}), 400 exam_code = data.get('exam_code', '').upper().strip() student_name = data.get('student_name', '').strip() print(f"📝 Join exam request - Code: {exam_code}, Name: {student_name}") # Enhanced validation with detailed error messages if not exam_code: print("❌ Missing exam_code") return jsonify({"error": "Exam code is required"}), 400 if not student_name: print("❌ Missing student_name") return jsonify({"error": "Student name is required"}), 400 # Check if exam exists exam = db.exams.find_one({"exam_code": exam_code}) if not exam: print(f"❌ Exam not found: {exam_code}") return jsonify({"error": "Invalid exam code"}), 404 print(f"✅ Found exam: {exam['title']} (Status: {exam['status']})") # Check exam status if exam['status'] == 'completed': print("❌ Exam already completed") return jsonify({"error": "This exam has already ended"}), 400 # Check capacity current_participants = exam.get('participants', []) max_participants = exam.get('max_participants', 50) if len(current_participants) >= max_participants: print(f"❌ Exam full: {len(current_participants)}/{max_participants}") return jsonify({"error": "Exam is full"}), 400 # Check if name is already taken existing_names = [p['name'].lower() for p in current_participants] if student_name.lower() in existing_names: print(f"❌ Name already taken: {student_name}") return jsonify({"error": "Name already taken. Please choose a different name."}), 400 # Create new participant participant = { "name": student_name, "joined_at": datetime.now(), "session_id": str(uuid.uuid4()), "score": 0, "submission": None, "language": None, "submission_time": None, "completed": False, "rank": 0, "test_results": [] } # Add participant to exam result = db.exams.update_one( {"exam_code": exam_code}, {"$push": {"participants": participant}} ) if result.modified_count == 0: print("❌ Failed to add participant to database") return jsonify({"error": "Failed to join exam"}), 500 # Set session data session['exam_code'] = exam_code session['student_name'] = student_name session['session_id'] = participant['session_id'] print(f"✅ Participant {student_name} joined exam {exam_code}") identity = resolve_user_identity(request, db) log_user_activity( db, identity.get("user_id"), "exam", "Joined coding exam", f"Joined exam '{exam.get('title', exam_code)}' as {student_name}", { "exam_code": exam_code, "exam_title": exam.get("title"), "student_name": student_name, "session_id": participant.get("session_id"), }, ) return jsonify({ "success": True, "message": f"Successfully joined exam: {exam['title']}", "exam_info": { "title": exam['title'], "duration_minutes": exam['duration_minutes'], "status": exam['status'], "participants_count": len(current_participants) + 1, "max_participants": max_participants, "languages": exam.get('problem', {}).get('languages', ['python']), "problem_title": exam.get('problem', {}).get('title', '') } }) except Exception as e: print(f"❌ Error joining exam: {str(e)}") import traceback traceback.print_exc() return jsonify({"error": f"Failed to join exam: {str(e)}"}), 500 @bp.route("/start-exam", methods=["POST", "OPTIONS"]) def start_exam(): """Host starts the exam""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response try: data = request.json exam_code = data.get('exam_code') print(f"📝 Start exam request - Code: {exam_code}") exam = db.exams.find_one({"exam_code": exam_code}) if not exam: return jsonify({"error": "Exam not found"}), 404 if exam['status'] != 'waiting': return jsonify({"error": "Exam has already started or ended"}), 400 start_time = datetime.now() end_time = start_time + timedelta(minutes=exam['duration_minutes']) db.exams.update_one( {"exam_code": exam_code}, { "$set": { "status": "active", "start_time": start_time, "end_time": end_time } } ) print(f"✅ Exam {exam_code} started successfully") return jsonify({ "success": True, "message": "Exam started successfully!", "start_time": start_time.isoformat(), "end_time": end_time.isoformat(), "participants_count": len(exam.get('participants', [])) }) except Exception as e: print(f"❌ Error starting exam: {str(e)}") return jsonify({"error": str(e)}), 500 # ✅ CRITICAL: The submit-solution route with enhanced debugging @bp.route('/submit-solution', methods=['POST', 'OPTIONS']) def submit_solution(): """Submit coding solution for evaluation - WITH DEBUG LOGGING""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response try: # ✅ ENHANCED DEBUG LOGGING print(f"🔍 ===== SUBMIT SOLUTION DEBUG =====") print(f"🔍 Raw request data: {request.data}") print(f"🔍 Content-Type: {request.headers.get('Content-Type')}") print(f"🔍 Request form: {dict(request.form) if request.form else 'None'}") print(f"🔍 Request args: {dict(request.args) if request.args else 'None'}") print(f"🔍 Request method: {request.method}") # Try to get JSON data try: data = request.get_json() print(f"🔍 Parsed JSON data: {data}") except Exception as json_error: print(f"🔍 JSON parsing error: {json_error}") data = None # Check what we actually received if not data: print("❌ No JSON data received") # Try alternative data sources if request.form: print("🔍 Trying to use form data instead...") data = { 'exam_code': request.form.get('exam_code'), 'username': request.form.get('username'), 'code': request.form.get('code'), 'language': request.form.get('language', 'python'), 'problem_id': request.form.get('problem_id', 'problem_1') } print(f"🔍 Form data converted: {data}") else: return jsonify({ "success": False, "error": "No JSON data received", "debug_info": { "content_type": request.headers.get('Content-Type'), "raw_data": request.data.decode() if request.data else None, "form_data": dict(request.form) if request.form else None, "suggestion": "Make sure Content-Type is 'application/json' and data is valid JSON" } }), 400 # Extract fields with detailed logging exam_code = data.get('exam_code') username = data.get('username') problem_id = data.get('problem_id', 'problem_1') code = data.get('code') language = data.get('language', 'python') print(f"🔍 Extracted fields:") print(f" - exam_code: '{exam_code}' (type: {type(exam_code)})") print(f" - username: '{username}' (type: {type(username)})") print(f" - problem_id: '{problem_id}' (type: {type(problem_id)})") print(f" - code: '{code[:50] if code else None}...' (length: {len(code) if code else 0})") print(f" - language: '{language}' (type: {type(language)})") # Enhanced validation with specific field checking missing_fields = [] if not exam_code or str(exam_code).strip() == '': missing_fields.append('exam_code') if not username or str(username).strip() == '': missing_fields.append('username') if not code or str(code).strip() == '': missing_fields.append('code') if missing_fields: print(f"❌ Missing fields: {missing_fields}") return jsonify({ "success": False, "error": f"Missing required fields: {', '.join(missing_fields)}", "received_data": data, "missing_fields": missing_fields, "debug_info": "Check that your frontend is sending all required fields with non-empty values" }), 400 print(f"📝 Solution submission: {username} -> {exam_code} (Problem: {problem_id})") # Find the exam exam = db.exams.find_one({"exam_code": exam_code.upper()}) if not exam: print(f"❌ Exam not found: {exam_code}") return jsonify({"success": False, "error": "Exam not found"}), 404 print(f"✅ Found exam: {exam['title']}") # Find the specific problem (support both old and new format) problem = None if exam.get('problems'): problem = next((p for p in exam.get('problems', []) if p.get('id') == problem_id), None) if not problem and exam.get('problem'): problem = exam['problem'] problem['id'] = 'problem_1' if not problem: print(f"❌ Problem not found: {problem_id}") return jsonify({"success": False, "error": "Problem not found"}), 404 print(f"✅ Found problem: {problem.get('title', 'Untitled')}") # Use the enhanced dynamic scoring system from main.py try: from main import calculate_dynamic_score print(f"🧮 Running dynamic scoring system...") result = calculate_dynamic_score(code, language, problem) print(f"🏆 Scoring result: {result['score']}% ({result['passed_tests']}/{result['total_tests']} tests)") except ImportError as e: print(f"⚠️ Could not import scoring system from main.py: {e}") # Fallback basic scoring if main function not available result = { 'score': 75, # Default score 'passed_tests': 1, 'total_tests': 1, 'test_results': [{'passed': True, 'description': 'Basic execution test', 'points_earned': 75}], 'execution_time': 0.1, 'details': {'points_earned': 75, 'total_points': 100} } print(f"🔄 Using fallback scoring: {result['score']}%") # Create submission record submission = { "submission_id": str(uuid.uuid4()), "exam_code": exam_code.upper(), "username": username, "problem_id": problem_id, "code": code, "language": language, "score": result['score'], "passed_tests": result['passed_tests'], "total_tests": result['total_tests'], "test_results": result['test_results'], "execution_time": result['execution_time'], "submitted_at": datetime.now(), "points_earned": result['details']['points_earned'], "total_points": result['details']['total_points'] } # Save submission to submissions collection db.submissions.insert_one(submission) print(f"💾 Submission saved to database") # Update participant in exam participant_update = { "name": username, "score": result['score'], "completed": True, "submission_time": datetime.now(), "language": language, "submission": code, "test_results": result['test_results'], "joined_at": datetime.now(), "session_id": str(uuid.uuid4()) } exam_update_result = db.exams.update_one( {"exam_code": exam_code.upper(), "participants.name": username}, {"$set": {f"participants.$": participant_update}} ) if exam_update_result.modified_count > 0: print(f"✅ Updated participant {username} in exam") else: print(f"⚠️ Could not update participant {username} in exam - may not exist") # Update participant leaderboard in separate collection participant_filter = {"exam_code": exam_code.upper(), "username": username} participant = db.participants.find_one(participant_filter) if participant: # Update existing participant total_score = participant.get('total_score', 0) + result['details']['points_earned'] problems_solved = participant.get('problems_solved', 0) if result['score'] == 100: # Perfect score problems_solved += 1 db.participants.update_one( participant_filter, { "$set": { "total_score": total_score, "problems_solved": problems_solved, "last_submission": datetime.now() }, "$push": { "submissions": { "problem_id": problem_id, "score": result['score'], "points": result['details']['points_earned'], "submitted_at": datetime.now() } } } ) print(f"✅ Updated existing participant record") else: # Create new participant new_participant = { "exam_code": exam_code.upper(), "username": username, "total_score": result['details']['points_earned'], "problems_solved": 1 if result['score'] == 100 else 0, "joined_at": datetime.now(), "last_submission": datetime.now(), "submissions": [{ "problem_id": problem_id, "score": result['score'], "points": result['details']['points_earned'], "submitted_at": datetime.now() }] } db.participants.insert_one(new_participant) print(f"✅ Created new participant record") print(f"✅ Solution submitted successfully: {result['score']}% ({result['passed_tests']}/{result['total_tests']} tests)") return jsonify({ "success": True, "message": f"Solution submitted successfully! Score: {result['score']}%", "result": { "score": result['score'], "passed_tests": result['passed_tests'], "total_tests": result['total_tests'], "test_results": result['test_results'], "execution_time": result['execution_time'], "points_earned": result['details']['points_earned'], "total_points": result['details']['total_points'], "language": language, "problem_id": problem_id } }) except Exception as e: print(f"❌ Submission error: {str(e)}") import traceback traceback.print_exc() return jsonify({ "success": False, "error": f"Server error: {str(e)}", "error_type": type(e).__name__ }), 500 @bp.route("/leaderboard/", methods=["GET", "OPTIONS"]) def get_leaderboard(exam_code): """Get real-time leaderboard visible to all participants""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") return response try: print(f"📝 Leaderboard request - Code: {exam_code}") exam = db.exams.find_one({"exam_code": exam_code.upper()}) if not exam: return jsonify({"error": "Exam not found"}), 404 participants = exam.get('participants', []) # Sort by score and submission time completed_participants = [p for p in participants if p.get('completed', False)] leaderboard = sorted( completed_participants, key=lambda x: (-x.get('score', 0), x.get('submission_time', datetime.now())) ) # Add rank to each participant for i, participant in enumerate(leaderboard): participant['rank'] = i + 1 waiting_participants = [p for p in participants if not p.get('completed', False)] # Calculate statistics total_score = sum(p.get('score', 0) for p in completed_participants) avg_score = total_score / len(completed_participants) if completed_participants else 0 return jsonify({ "success": True, "exam_info": { "title": exam['title'], "status": exam['status'], "duration_minutes": exam['duration_minutes'], "start_time": exam.get('start_time'), "end_time": exam.get('end_time'), "problem_title": exam.get('problem', {}).get('title', '') }, "leaderboard": leaderboard, "waiting_participants": waiting_participants, "stats": { "total_participants": len(participants), "completed_submissions": len(completed_participants), "waiting_submissions": len(waiting_participants), "average_score": round(avg_score, 1), "highest_score": max((p.get('score', 0) for p in completed_participants), default=0) } }) except Exception as e: print(f"❌ Error getting leaderboard: {str(e)}") return jsonify({"error": str(e)}), 500 @bp.route("/get-problem/", methods=["GET", "OPTIONS"]) def get_exam_problem(exam_code): """Get problem details for participants""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") return response try: exam = db.exams.find_one({"exam_code": exam_code.upper()}) if not exam: return jsonify({"error": "Exam not found"}), 404 return jsonify({ "success": True, "problem": exam.get('problem', {}), "exam_info": { "title": exam['title'], "status": exam['status'], "duration_minutes": exam['duration_minutes'] } }) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route("/host-dashboard/", methods=["GET", "OPTIONS"]) def get_host_dashboard(exam_code): """Get comprehensive host dashboard data""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") return response try: exam = db.exams.find_one({"exam_code": exam_code.upper()}) if not exam: return jsonify({"error": "Exam not found"}), 404 participants = exam.get('participants', []) # Separate participants by status completed_participants = [p for p in participants if p.get('completed', False)] waiting_participants = [p for p in participants if not p.get('completed', False)] # Sort leaderboard leaderboard = sorted( completed_participants, key=lambda x: (-x.get('score', 0), x.get('submission_time', datetime.now())) ) # Add ranks for i, participant in enumerate(leaderboard): participant['rank'] = i + 1 # Calculate time statistics current_time = datetime.now() start_time = exam.get('start_time') end_time = exam.get('end_time') time_elapsed = 0 time_remaining = 0 if start_time: time_elapsed = int((current_time - start_time).total_seconds()) if end_time and current_time < end_time: time_remaining = int((end_time - current_time).total_seconds()) return jsonify({ "success": True, "exam_info": { "exam_code": exam['exam_code'], "title": exam['title'], "status": exam['status'], "duration_minutes": exam['duration_minutes'], "max_participants": exam.get('max_participants', 50), "created_at": exam.get('created_at'), "start_time": start_time, "end_time": end_time, "time_elapsed": time_elapsed, "time_remaining": time_remaining }, "participants": { "total": len(participants), "completed": len(completed_participants), "working": len(waiting_participants), "all_participants": sorted(participants, key=lambda x: x.get('joined_at', datetime.now())), "recent_joins": sorted(participants, key=lambda x: x.get('joined_at', datetime.now()), reverse=True)[:5] }, "leaderboard": leaderboard, "statistics": { "average_score": sum(p.get('score', 0) for p in completed_participants) / len(completed_participants) if completed_participants else 0, "highest_score": max((p.get('score', 0) for p in completed_participants), default=0), "lowest_score": min((p.get('score', 0) for p in completed_participants), default=0), "completion_rate": (len(completed_participants) / len(participants) * 100) if participants else 0 }, "problem": exam.get('problem', {}) }) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/info/', methods=['GET', 'OPTIONS']) def get_exam_info(exam_code): """Get detailed information about an exam for the host panel""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") return response try: print(f"📊 Host panel requesting info for exam: {exam_code}") exam = db.exams.find_one({"exam_code": exam_code.upper()}) if not exam: print(f"❌ Exam not found: {exam_code}") return jsonify({"success": False, "error": "Exam not found"}), 404 # Convert datetime objects to strings for JSON serialization created_at = exam.get("created_at") if hasattr(created_at, 'isoformat'): created_at = created_at.isoformat() exam_info = { "title": exam["title"], "status": exam["status"], "duration_minutes": exam["duration_minutes"], "participants_count": len(exam.get("participants", [])), "max_participants": exam.get("max_participants", 50), "problem_title": exam.get("problem", {}).get("title", exam["title"]), "languages": exam.get("problem", {}).get("languages", ["python"]), "created_at": created_at, "host_name": exam["host_name"] } print(f"✅ Found exam: {exam['title']} (Status: {exam['status']})") return jsonify({"success": True, "exam_info": exam_info}) except Exception as e: print(f"❌ Error getting exam info: {str(e)}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @bp.route('/participants/', methods=['GET', 'OPTIONS']) def get_participants(exam_code): """Get list of participants for host panel monitoring""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "GET,OPTIONS") return response try: exam = db.exams.find_one({"exam_code": exam_code.upper()}) if not exam: return jsonify({"success": False, "error": "Exam not found"}), 404 participants = exam.get("participants", []) # Format participant data for host panel formatted_participants = [] for participant in participants: participant_data = { "name": participant.get("name", ""), "score": participant.get("score", 0), "completed": participant.get("completed", False), "joined_at": participant.get("joined_at", ""), "submitted_at": participant.get("submitted_at", None) } formatted_participants.append(participant_data) print(f"👥 Retrieved {len(formatted_participants)} participants for exam {exam_code}") return jsonify({"success": True, "participants": formatted_participants}) except Exception as e: print(f"❌ Error getting participants: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @bp.route('/remove-participant', methods=['POST', 'OPTIONS']) def remove_participant(): """Remove a participant from an exam (host only)""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response try: data = request.get_json() exam_code = data.get('exam_code', '').upper() participant_name = data.get('participant_name', '') if not exam_code or not participant_name: return jsonify({"success": False, "error": "Missing exam_code or participant_name"}), 400 # Remove participant from exam result = db.exams.update_one( {"exam_code": exam_code}, {"$pull": {"participants": {"name": participant_name}}} ) if result.modified_count > 0: print(f"🗑️ Host removed participant {participant_name} from exam {exam_code}") return jsonify({"success": True, "message": f"Participant {participant_name} removed successfully"}) else: return jsonify({"success": False, "error": "Participant not found or already removed"}), 404 except Exception as e: print(f"❌ Error removing participant: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @bp.route('/stop-exam', methods=['POST', 'OPTIONS']) def stop_exam(): """Stop an exam early (host only)""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response try: data = request.get_json() exam_code = data.get('exam_code', '').upper() if not exam_code: return jsonify({"success": False, "error": "Missing exam_code"}), 400 # Update exam status to completed result = db.exams.update_one( {"exam_code": exam_code}, {"$set": { "status": "completed", "ended_at": datetime.now().isoformat(), "ended_by": "host" }} ) if result.modified_count > 0: print(f"🛑 Exam {exam_code} stopped early by host") return jsonify({"success": True, "message": "Exam stopped successfully"}) else: return jsonify({"success": False, "error": "Exam not found"}), 404 except Exception as e: print(f"❌ Error stopping exam: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @bp.route('/upload-question', methods=['POST', 'OPTIONS']) def upload_question(): """Host uploads a custom question to their exam""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response try: data = request.get_json() exam_code = data.get('exam_code', '').upper() question_data = data.get('question', {}) print(f"📤 Host uploading question to exam: {exam_code}") if not exam_code or not question_data: return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400 # Validate required question fields required_fields = ['title', 'description'] for field in required_fields: if not question_data.get(field): return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400 # Find the exam exam = db.exams.find_one({"exam_code": exam_code}) if not exam: return jsonify({"success": False, "error": "Exam not found"}), 404 # Check if exam has already started if exam['status'] != 'waiting': return jsonify({"success": False, "error": "Cannot modify questions after exam has started"}), 400 # Generate question ID question_id = str(uuid.uuid4()) # Prepare question document question = { "id": question_id, "title": question_data['title'], "description": question_data['description'], "difficulty": question_data.get('difficulty', 'medium'), "function_name": question_data.get('function_name', 'solve'), "starter_code": question_data.get('starter_code', { 'python': f'def {question_data.get("function_name", "solve")}():\n # Write your solution here\n pass' }), "test_cases": question_data.get('test_cases', []), "examples": question_data.get('examples', []), "constraints": question_data.get('constraints', []), "time_limit": question_data.get('time_limit', 1000), "memory_limit": question_data.get('memory_limit', '128MB'), "created_at": datetime.now(), "uploaded_by": exam.get('host_name', 'Unknown'), "languages": question_data.get('languages', ['python']), "total_points": question_data.get('total_points', 100) } # Update the exam with the new question result = db.exams.update_one( {"exam_code": exam_code}, { "$set": { "problem": question, "updated_at": datetime.now() } } ) if result.modified_count > 0: print(f"✅ Question '{question['title']}' uploaded to exam {exam_code}") return jsonify({ "success": True, "message": "Question uploaded successfully", "question_id": question_id, "question_title": question['title'] }) else: return jsonify({"success": False, "error": "Failed to update exam"}), 500 except Exception as e: print(f"❌ Error uploading question: {str(e)}") import traceback traceback.print_exc() return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500 @bp.route('/update-duration', methods=['POST', 'OPTIONS']) def update_duration(): """Update exam duration (host only, before exam starts)""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response try: data = request.get_json() exam_code = data.get('exam_code', '').upper() duration_minutes = data.get('duration_minutes', 0) print(f"⏰ Updating duration for exam {exam_code} to {duration_minutes} minutes") if not exam_code or duration_minutes <= 0: return jsonify({"success": False, "error": "Invalid exam_code or duration_minutes"}), 400 # Find the exam exam = db.exams.find_one({"exam_code": exam_code}) if not exam: return jsonify({"success": False, "error": "Exam not found"}), 404 # Check if exam can be modified if exam.get('status') != 'waiting': return jsonify({"success": False, "error": "Cannot modify duration after exam has started"}), 400 # Update duration result = db.exams.update_one( {"exam_code": exam_code}, { "$set": { "duration_minutes": duration_minutes, "updated_at": datetime.now() } } ) if result.modified_count > 0: print(f"✅ Duration updated to {duration_minutes} minutes for exam {exam_code}") return jsonify({ "success": True, "message": f"Duration updated to {duration_minutes} minutes", "new_duration": duration_minutes }) else: return jsonify({"success": False, "error": "Failed to update duration"}), 500 except Exception as e: print(f"❌ Error updating duration: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @bp.route("/debug-join-data", methods=["POST", "OPTIONS"]) def debug_join_data(): """Debug what data is actually being received""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization") response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") return response print(f"🔍 Raw request data: {request.data}") print(f"🔍 Request JSON: {request.json}") print(f"🔍 Content-Type: {request.headers.get('Content-Type')}") return jsonify({ "received_raw": request.data.decode() if request.data else None, "received_json": request.json, "content_type": request.headers.get('Content-Type'), "success": True }) @bp.route("/test", methods=["GET"]) def test_exam_route(): """Test if exam routes are working""" return jsonify({ "success": True, "message": "Exam routes are working", "timestamp": datetime.now().isoformat(), "available_routes": [ "/api/exam/create-exam", "/api/exam/join-exam", "/api/exam/start-exam", "/api/exam/submit-solution", # ✅ Now included! "/api/exam/leaderboard/", "/api/exam/get-problem/", "/api/exam/host-dashboard/", "/api/exam/info/", "/api/exam/participants/", "/api/exam/remove-participant", "/api/exam/stop-exam", "/api/exam/upload-question", "/api/exam/update-duration", "/api/exam/debug-join-data" ] }) @bp.route("/", methods=["GET"]) def exam_root(): """Exam route root""" return jsonify({ "message": "OpenLearnX Exam API", "available_endpoints": [ "/api/exam/create-exam", "/api/exam/join-exam", "/api/exam/start-exam", "/api/exam/submit-solution", # ✅ Now included! "/api/exam/leaderboard/", "/api/exam/get-problem/", "/api/exam/host-dashboard/", "/api/exam/info/", "/api/exam/participants/", "/api/exam/remove-participant", "/api/exam/stop-exam", "/api/exam/upload-question", "/api/exam/update-duration", "/api/exam/test", "/api/exam/debug-join-data" ] })