From 0a63d19b597d7ad6729e2a594af85ea8123de89c Mon Sep 17 00:00:00 2001 From: 5t4l1n Date: Sun, 27 Jul 2025 03:54:54 +0530 Subject: [PATCH] update & add --- backend/backend/deployment.json | 690 +++++++++++ backend/backend/foundry.toml | 11 + backend/backend/main.py | 1007 +++++++++++++++++ backend/backend/models/user.py | 30 + backend/backend/mongo_service.py | 142 +++ backend/backend/routes/admin.py | 428 +++++++ backend/backend/routes/auth.py | 90 ++ backend/backend/routes/certificate.py | 49 + backend/backend/routes/coding.py | 240 ++++ backend/backend/routes/compiler.py | 546 +++++++++ backend/backend/routes/courses.py | 93 ++ backend/backend/routes/dashboard.py | 40 + backend/backend/routes/exam.py | 931 +++++++++++++++ backend/backend/routes/quizzes.py | 32 + backend/backend/routes/test_flow.py | 108 ++ backend/backend/seed_courses.py | 111 ++ backend/backend/services/compiler_service.py | 42 + .../backend/services/real_compiler_service.py | 305 +++++ backend/backend/services/wallet_service.py | 53 + backend/backend/utils/adaptive_engine.py | 8 + backend/backend/web3_service.py | 283 +++++ backend/main.py | 796 ++++--------- frontend/app/coding/exam/[examCode]/page.tsx | 489 +++++--- frontend/app/coding/host/[examCode]/page.tsx | 727 ++++++++---- 24 files changed, 6298 insertions(+), 953 deletions(-) create mode 100644 backend/backend/deployment.json create mode 100644 backend/backend/foundry.toml create mode 100644 backend/backend/main.py create mode 100644 backend/backend/models/user.py create mode 100644 backend/backend/mongo_service.py create mode 100644 backend/backend/routes/admin.py create mode 100644 backend/backend/routes/auth.py create mode 100644 backend/backend/routes/certificate.py create mode 100644 backend/backend/routes/coding.py create mode 100644 backend/backend/routes/compiler.py create mode 100644 backend/backend/routes/courses.py create mode 100644 backend/backend/routes/dashboard.py create mode 100644 backend/backend/routes/exam.py create mode 100644 backend/backend/routes/quizzes.py create mode 100644 backend/backend/routes/test_flow.py create mode 100644 backend/backend/seed_courses.py create mode 100644 backend/backend/services/compiler_service.py create mode 100644 backend/backend/services/real_compiler_service.py create mode 100644 backend/backend/services/wallet_service.py create mode 100644 backend/backend/utils/adaptive_engine.py create mode 100644 backend/backend/web3_service.py diff --git a/backend/backend/deployment.json b/backend/backend/deployment.json new file mode 100644 index 0000000..63b55a3 --- /dev/null +++ b/backend/backend/deployment.json @@ -0,0 +1,690 @@ +{ + "contract_address": "0xC2FE2F49B3a1384aEdFAae127F054FAf216eF684", + "transaction_hash": "0xfe5a433dae316bd2d60b7190c21866a1fde30777f08d9d37e403ed642433fa28", + "deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "network": "local", + "abi": [ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "certificates", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "subject", + "type": "string", + "internalType": "string" + }, + { + "name": "studentName", + "type": "string", + "internalType": "string" + }, + { + "name": "score", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "timestamp", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "verified", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getApproved", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCertificate", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct CertificateNFT.Certificate", + "components": [ + { + "name": "subject", + "type": "string", + "internalType": "string" + }, + { + "name": "studentName", + "type": "string", + "internalType": "string" + }, + { + "name": "score", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "timestamp", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "verified", + "type": "bool", + "internalType": "bool" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getUserCertificates", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isApprovedForAll", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mintCertificate", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "_tokenURI", + "type": "string", + "internalType": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "mintCertificateWithDetails", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "_tokenURI", + "type": "string", + "internalType": "string" + }, + { + "name": "subject", + "type": "string", + "internalType": "string" + }, + { + "name": "studentName", + "type": "string", + "internalType": "string" + }, + { + "name": "score", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "ownerOf", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "safeTransferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "safeTransferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setApprovalForAll", + "inputs": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "approved", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "tokenURI", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "userCertificates", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "verifyCertificate", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "approved", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ApprovalForAll", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "approved", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "BatchMetadataUpdate", + "inputs": [ + { + "name": "_fromTokenId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "_toTokenId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CertificateMinted", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "student", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "subject", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "score", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "tokenURI", + "type": "string", + "indexed": false, + "internalType": "string" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MetadataUpdate", + "inputs": [ + { + "name": "_tokenId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + } + ], + "gas_used": 3387337, + "block_number": 22994809, + "status": 1 +} \ No newline at end of file diff --git a/backend/backend/foundry.toml b/backend/backend/foundry.toml new file mode 100644 index 0000000..b6b4a44 --- /dev/null +++ b/backend/backend/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = "contracts" +out = "out" +libs = ["lib"] +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/" +] + +[rpc_endpoints] +local = "http://127.0.0.1:8545" +sepolia = "https://sepolia.infura.io/v3/${INFURA_API_KEY}" diff --git a/backend/backend/main.py b/backend/backend/main.py new file mode 100644 index 0000000..3655a54 --- /dev/null +++ b/backend/backend/main.py @@ -0,0 +1,1007 @@ +import os +import asyncio +import logging +import uuid +from datetime import datetime +from flask import Flask, jsonify, request +from flask_cors import CORS +from dotenv import load_dotenv +from pymongo import MongoClient +from bson import ObjectId + +# Load env vars +load_dotenv() + +# Services +from mongo_service import MongoService +from web3_service import Web3Service + +# Blueprints +from routes.auth import bp as auth_bp +from routes.test_flow import bp as test_flow_bp +from routes.certificate import bp as cert_bp +from routes.dashboard import bp as dash_bp +from routes.courses import bp as courses_bp +from routes.quizzes import bp as quizzes_bp +from routes.admin import bp as admin_bp +from routes.exam import bp as exam_bp +from routes.compiler import bp as compiler_bp + +# Optional services +try: + from services.wallet_service import wallet_service + WALLET_SERVICE_AVAILABLE = True +except ImportError: + wallet_service = None + WALLET_SERVICE_AVAILABLE = False + +try: + from services.real_compiler_service import real_compiler_service + COMPILER_SERVICE_AVAILABLE = True +except ImportError: + real_compiler_service = None + COMPILER_SERVICE_AVAILABLE = False + +# Flask app +app = Flask(__name__) +app.config.update( + SECRET_KEY=os.getenv('SECRET_KEY', 'openlearnx-secret'), + MONGODB_URI=os.getenv('MONGODB_URI', 'mongodb://localhost:27017/'), + WEB3_PROVIDER_URL=os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545'), + CONTRACT_ADDRESS=os.getenv('CONTRACT_ADDRESS', ''), + ADMIN_TOKEN=os.getenv('ADMIN_TOKEN', '') +) + +# CORS +CORS(app, resources={r"/api/*": { + "origins": ["http://localhost:3000", "http://127.0.0.1:3000"], + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization", "Accept", "Origin", "X-Requested-With"], + "supports_credentials": True, + "expose_headers": ["Authorization"] +}}) + +# Logging +logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + handlers=[logging.StreamHandler()]) +logger = logging.getLogger(__name__) + +# Initialize services +try: + mongo_service = MongoService(app.config['MONGODB_URI']) + app.config['MONGO_SERVICE'] = mongo_service + MONGO_SERVICE_AVAILABLE = True + logger.info("✅ MongoService initialized") +except Exception as e: + MONGO_SERVICE_AVAILABLE = False + logger.error(f"❌ Failed MongoService init: {e}") + +try: + web3_service = Web3Service(app.config['WEB3_PROVIDER_URL'], app.config['CONTRACT_ADDRESS']) + app.config['WEB3_SERVICE'] = web3_service + WEB3_SERVICE_AVAILABLE = True + logger.info("✅ Web3Service initialized") +except Exception as e: + WEB3_SERVICE_AVAILABLE = False + logger.error(f"❌ Failed Web3Service init: {e}") + +if WALLET_SERVICE_AVAILABLE: + app.config['WALLET_SERVICE'] = wallet_service +if COMPILER_SERVICE_AVAILABLE: + app.config['REAL_COMPILER_SERVICE'] = real_compiler_service + +def check_docker_availability(): + try: + import docker + docker.from_env().ping() + return True + except: + return False + +# Register blueprints +blueprints_registered = [] +blueprints_failed = [] + +for bp, prefix in [ + (auth_bp, '/api/auth'), + (test_flow_bp, '/api/test'), + (cert_bp, '/api/certificate'), + (dash_bp, '/api/dashboard'), + (courses_bp, '/api/courses'), + (quizzes_bp, '/api/quizzes'), + (admin_bp, '/api/admin'), + (exam_bp, '/api/exam'), + (compiler_bp, '/api/compiler'), +]: + try: + app.register_blueprint(bp, url_prefix=prefix) + blueprints_registered.append(prefix) + logger.info(f"✅ Registered blueprint {prefix}") + except Exception as e: + blueprints_failed.append((prefix, str(e))) + logger.error(f"❌ Blueprint {prefix} failed: {e}") + +# Database connection +def get_db(): + """Get MongoDB database connection""" + client = MongoClient(app.config['MONGODB_URI']) + return client.openlearnx + +# =================================================================== +# ✅ DYNAMIC SCORING SYSTEM +# =================================================================== + +def calculate_dynamic_score(code, language, problem): + """Calculate score based on test cases and expected outputs""" + import io + from contextlib import redirect_stdout, redirect_stderr + import time + + test_cases = problem.get('test_cases', []) + total_points = problem.get('total_points', 100) + + start_time = time.time() + passed_tests = 0 + total_tests = len(test_cases) if test_cases else 1 + test_results = [] + points_earned = 0 + + print(f"🧮 Dynamic scoring - {total_tests} test cases, {total_points} total points") + + try: + if test_cases: + for i, test_case in enumerate(test_cases): + test_input = test_case.get('input', '') + expected_output = test_case.get('expected_output', '').strip() + test_points = test_case.get('points', total_points // total_tests) + + print(f"📋 Test {i+1}: Input='{test_input}', Expected='{expected_output}', Points={test_points}") + + try: + stdout_buffer = io.StringIO() + stderr_buffer = io.StringIO() + + exec_globals = {"__builtins__": __builtins__} + if test_input: + exec_globals['input'] = lambda prompt='': test_input + + with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): + exec(code, exec_globals) + + actual_output = stdout_buffer.getvalue().strip() + + print(f"🔍 Test {i+1} - Actual: '{actual_output}', Expected: '{expected_output}'") + + if actual_output == expected_output: + passed_tests += 1 + points_earned += test_points + test_results.append({ + "test_number": i + 1, + "passed": True, + "input": test_input, + "expected_output": expected_output, + "actual_output": actual_output, + "points_earned": test_points, + "description": test_case.get('description', f'Test case {i+1}') + }) + print(f"✅ Test {i+1} PASSED - {test_points} points earned") + else: + test_results.append({ + "test_number": i + 1, + "passed": False, + "input": test_input, + "expected_output": expected_output, + "actual_output": actual_output, + "points_earned": 0, + "error": f"Output mismatch. Got '{actual_output}', expected '{expected_output}'", + "description": test_case.get('description', f'Test case {i+1}') + }) + print(f"❌ Test {i+1} FAILED - Expected '{expected_output}', got '{actual_output}'") + + except Exception as e: + print(f"❌ Test {i+1} EXCEPTION - {str(e)}") + test_results.append({ + "test_number": i + 1, + "passed": False, + "input": test_input, + "expected_output": expected_output, + "actual_output": f"Error: {str(e)}", + "points_earned": 0, + "error": str(e), + "description": test_case.get('description', f'Test case {i+1}') + }) + else: + # Fallback: Basic execution test + try: + stdout_buffer = io.StringIO() + stderr_buffer = io.StringIO() + + with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): + exec(code, {"__builtins__": __builtins__}) + + passed_tests = 1 + points_earned = total_points + test_results = [{ + "test_number": 1, + "passed": True, + "input": "", + "expected_output": "Code should execute without errors", + "actual_output": stdout_buffer.getvalue().strip(), + "points_earned": total_points, + "description": "Basic execution test" + }] + except Exception as e: + test_results = [{ + "test_number": 1, + "passed": False, + "input": "", + "expected_output": "Code should execute without errors", + "actual_output": f"Error: {str(e)}", + "points_earned": 0, + "error": str(e), + "description": "Basic execution test" + }] + + except Exception as e: + print(f"❌ Scoring system error: {str(e)}") + test_results = [{ + "test_number": 1, + "passed": False, + "input": "", + "expected_output": "Code should execute without errors", + "actual_output": f"Scoring error: {str(e)}", + "points_earned": 0, + "error": str(e), + "description": "Scoring system error" + }] + + execution_time = time.time() - start_time + final_score = int((points_earned / total_points) * 100) if total_points > 0 else 0 + + print(f"🏆 FINAL SCORE: {final_score}% ({points_earned}/{total_points} points, {passed_tests}/{total_tests} tests)") + + return { + 'score': final_score, + 'passed_tests': passed_tests, + 'total_tests': total_tests, + 'test_results': test_results, + 'execution_time': round(execution_time, 3), + 'details': { + 'points_earned': points_earned, + 'total_points': total_points, + 'scoring_method': 'test_cases' + } + } + +# =================================================================== +# ✅ SUBMIT SOLUTION WITH GUARANTEED LEADERBOARD UPDATE +# =================================================================== + +@app.route('/api/exam/submit-solution', methods=['POST', 'OPTIONS']) +def submit_solution_direct(): + """Submit solution with guaranteed leaderboard update""" + 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() + language = data.get('language', 'python') + code = data.get('code', '').strip() + participant_name = data.get('participant_name', 'Anonymous').strip() + + print(f"📤 SUBMIT: Exam {exam_code}, Participant: '{participant_name}'") + + if not exam_code or not code or not participant_name: + return jsonify({"success": False, "error": "Missing required data"}), 400 + + db = get_db() + + # Find the exam + exam = db.exams.find_one({"exam_code": exam_code}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + if exam.get('status') != 'active': + return jsonify({"success": False, "error": "Exam is not active"}), 400 + + # Get the problem/question + problem = exam.get('problem', {}) + if not problem: + return jsonify({"success": False, "error": "No problem found for this exam"}), 400 + + # Check for existing submission + existing_submission = db.submissions.find_one({ + "exam_code": exam_code, + "participant_name": participant_name + }) + + if existing_submission: + print(f"⚠️ Participant {participant_name} already has submission") + return jsonify({"success": False, "error": f"Participant '{participant_name}' has already submitted"}), 400 + + # Calculate score + scoring_result = calculate_dynamic_score(code, language, problem) + + # Store submission + submission = { + "exam_code": exam_code, + "participant_name": participant_name, + "language": language, + "code": code, + "score": scoring_result['score'], + "passed_tests": scoring_result['passed_tests'], + "total_tests": scoring_result['total_tests'], + "test_results": scoring_result['test_results'], + "execution_time": scoring_result['execution_time'], + "submitted_at": datetime.now(), + "submission_id": str(uuid.uuid4()), + "scoring_details": scoring_result['details'] + } + + # Save submission to database + submission_result = db.submissions.insert_one(submission) + print(f"💾 Submission saved with ID: {submission_result.inserted_id}") + + # Delete old participant record and create new completed one + print(f"🗑️ Deleting any existing participant records for {participant_name}") + delete_result = db.participants.delete_many({"exam_code": exam_code, "name": participant_name}) + print(f"🗑️ Deleted {delete_result.deleted_count} old participant records") + + # Create fresh completed participant record + participant_record = { + "exam_code": exam_code, + "name": participant_name, + "completed": True, # CRITICAL: Must be True + "score": scoring_result['score'], + "submitted_at": datetime.now(), + "joined_at": datetime.now(), + "language": language, + "passed_tests": scoring_result['passed_tests'], + "total_tests": scoring_result['total_tests'], + "points_earned": scoring_result['details']['points_earned'], + "total_points": scoring_result['details']['total_points'], + "session_id": str(uuid.uuid4()), + "rank": 0, + "updated_at": datetime.now() + } + + # Insert fresh participant record + participant_result = db.participants.insert_one(participant_record) + print(f"👤 NEW participant record created with ID: {participant_result.inserted_id}") + + # Verification + verification = db.participants.find_one({"exam_code": exam_code, "name": participant_name}) + if verification and verification.get('completed'): + print(f"✅ VERIFICATION SUCCESS: {participant_name} completed={verification.get('completed')}, score={verification.get('score')}") + else: + print(f"❌ VERIFICATION FAILED: Participant record not found or not completed") + return jsonify({"success": False, "error": "Failed to update participant status"}), 500 + + print(f"✅ SUBMIT COMPLETE - {participant_name}: {scoring_result['score']}% ({scoring_result['passed_tests']}/{scoring_result['total_tests']} tests)") + + return jsonify({ + "success": True, + "message": f"Solution submitted successfully for {participant_name}!", + "score": scoring_result['score'], + "passed_tests": scoring_result['passed_tests'], + "total_tests": scoring_result['total_tests'], + "test_results": scoring_result['test_results'], + "execution_time": scoring_result['execution_time'], + "submission_id": submission["submission_id"], + "scoring_details": scoring_result['details'], + "participant_name": participant_name, + "leaderboard_updated": True + }) + + except Exception as e: + print(f"❌ Submit error: {str(e)}") + import traceback + traceback.print_exc() + return jsonify({"success": False, "error": f"Submission failed: {str(e)}"}), 500 + +# =================================================================== +# ✅ ULTIMATE LEADERBOARD FIX - Handles duplicates and forces sync +# =================================================================== + +@app.route('/api/exam/leaderboard/', methods=['GET', 'OPTIONS']) +def get_leaderboard_direct(exam_code): + """ULTIMATE LEADERBOARD FIX - Handles duplicates and forces correct sync""" + 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"🏆 ULTIMATE LEADERBOARD FIX: {exam_code}") + db = get_db() + + # Get exam info + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + # ✅ ULTIMATE FIX: Get unique submissions (latest per participant) using aggregation + print(f"🔄 Getting latest submission per participant...") + + # Use MongoDB aggregation to get latest submission per participant + pipeline = [ + {"$match": {"exam_code": exam_code.upper()}}, + {"$sort": {"submitted_at": -1}}, # Sort by latest first + {"$group": { + "_id": "$participant_name", # Group by participant name + "latest_submission": {"$first": "$$ROOT"} # Take the first (latest) submission + }}, + {"$replaceRoot": {"newRoot": "$latest_submission"}} # Replace root with the submission + ] + + unique_submissions = list(db.submissions.aggregate(pipeline)) + print(f"📋 Found {len(unique_submissions)} unique submissions after deduplication") + + # Debug: Print unique submissions + for sub in unique_submissions: + print(f" - {sub.get('participant_name')}: {sub.get('score')}% at {sub.get('submitted_at')}") + + # ✅ FORCE REBUILD: Delete ALL participants and recreate from unique submissions + delete_result = db.participants.delete_many({"exam_code": exam_code.upper()}) + print(f"🗑️ Deleted {delete_result.deleted_count} old participant records") + + # Create leaderboard from unique submissions + leaderboard = [] + for submission in unique_submissions: + participant_name = submission.get('participant_name') + if not participant_name: + continue + + # Create completed participant record + participant = { + "exam_code": exam_code.upper(), + "name": participant_name, + "completed": True, # ✅ ALWAYS TRUE + "score": submission.get('score', 0), + "submitted_at": submission.get('submitted_at'), + "joined_at": submission.get('submitted_at'), + "language": submission.get('language'), + "passed_tests": submission.get('passed_tests', 0), + "total_tests": submission.get('total_tests', 1), + "points_earned": submission.get('scoring_details', {}).get('points_earned', 0), + "total_points": submission.get('scoring_details', {}).get('total_points', 100), + "session_id": f"ultimate-{uuid.uuid4()}", + "rank": 0 # Will be set below + } + + # Insert participant and add to leaderboard + result = db.participants.insert_one(participant) + participant['_id'] = str(result.inserted_id) + leaderboard.append(participant) + + print(f"✅ CREATED: {participant_name} with score {submission.get('score', 0)}%") + + # Sort by score (highest first) and assign ranks + leaderboard.sort(key=lambda x: x.get('score', 0), reverse=True) + for i, participant in enumerate(leaderboard): + participant['rank'] = i + 1 + # Update rank in database + db.participants.update_one( + {"_id": ObjectId(participant['_id'])}, + {"$set": {"rank": i + 1}} + ) + + # Calculate accurate stats + scores = [p.get('score', 0) for p in leaderboard] + passed_tests = [p.get('passed_tests', 0) for p in leaderboard] + total_tests = [p.get('total_tests', 1) for p in leaderboard] + + stats = { + "total_participants": len(leaderboard), + "completed_submissions": len(leaderboard), + "waiting_submissions": 0, # No waiting since we only show submitted participants + "average_score": round(sum(scores) / len(scores)) if scores else 0, + "highest_score": max(scores) if scores else 0, + "average_tests_passed": round(sum(passed_tests) / len(passed_tests)) if passed_tests else 0, + "total_test_cases": max(total_tests) if total_tests else 1, + "blockchain_participants": 0 + } + + if '_id' in exam: + exam['_id'] = str(exam['_id']) + + print(f"🎯 ULTIMATE LEADERBOARD COMPLETE:") + print(f" - Unique participants: {len(leaderboard)}") + print(f" - Waiting participants: 0") + print(f" - Average score: {stats['average_score']}%") + + return jsonify({ + "success": True, + "leaderboard": leaderboard, + "waiting_participants": [], # Always empty - only show completed participants + "stats": stats, + "exam_info": exam, + "ultimate_fix_applied": True, + "unique_submissions_processed": len(unique_submissions) + }) + + except Exception as e: + print(f"❌ Ultimate leaderboard error: {str(e)}") + import traceback + traceback.print_exc() + return jsonify({"success": False, "error": str(e)}), 500 + +# =================================================================== +# ✅ OTHER EXAM ENDPOINTS +# =================================================================== + +@app.route('/api/exam/upload-question', methods=['POST', 'OPTIONS']) +def upload_question_direct(): + """Enhanced question upload with dynamic scoring""" + 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', {}) + + if not exam_code or not question_data: + return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400 + + db = get_db() + + # 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 questions after exam has started"}), 400 + + # Enhanced question structure + question = { + "id": str(uuid.uuid4()), + "title": question_data.get('title', 'Custom Question'), + "description": question_data.get('description', 'Custom programming question'), + "difficulty": question_data.get('difficulty', 'medium'), + "function_name": question_data.get('function_name', 'solve'), + "starter_code": question_data.get('starter_code', { + 'python': 'def solve():\n # Write your solution here\n pass' + }), + "test_cases": question_data.get('test_cases', [ + { + "input": "", + "expected_output": "Hello World", + "description": "Basic test case", + "is_public": True, + "points": 100 + } + ]), + "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', 'Host'), + "languages": list(question_data.get('starter_code', {}).keys()), + "correct_solution": { + "python": question_data.get('correct_solution', {}).get('python', ''), + "java": question_data.get('correct_solution', {}).get('java', ''), + "javascript": question_data.get('correct_solution', {}).get('javascript', '') + }, + "scoring_method": question_data.get('scoring_method', 'test_cases'), + "total_points": question_data.get('total_points', 100) + } + + # Update exam + result = db.exams.update_one( + {"exam_code": exam_code}, + { + "$set": { + "problem": question, + "problem_title": question['title'], + "updated_at": datetime.now() + } + } + ) + + if result.modified_count > 0: + return jsonify({ + "success": True, + "message": "Question uploaded successfully with dynamic scoring", + "question_id": question['id'], + "question_title": question['title'], + "test_cases_count": len(question['test_cases']), + "total_points": question['total_points'] + }) + else: + return jsonify({"success": False, "error": "Failed to update exam"}), 500 + + except Exception as e: + import traceback + traceback.print_exc() + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/exam/info/', methods=['GET', 'OPTIONS']) +def get_exam_info_direct(exam_code): + """Get exam information""" + 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: + db = get_db() + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + if '_id' in exam: + exam['_id'] = str(exam['_id']) + + return jsonify({ + "success": True, + "exam_info": exam + }) + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/exam/get-problem/', methods=['GET', 'OPTIONS']) +def get_exam_problem_direct(exam_code): + """Get exam problem""" + 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: + db = get_db() + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + if '_id' in exam: + exam['_id'] = str(exam['_id']) + + return jsonify({ + "success": True, + "exam_info": exam, + "problem": exam.get('problem', {}) + }) + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/exam/start-exam', methods=['POST', 'OPTIONS']) +def start_exam_direct(): + """Start an 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() + + db = get_db() + exam = db.exams.find_one({"exam_code": exam_code}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + if exam.get('status') != 'waiting': + return jsonify({"success": False, "error": "Exam already started or completed"}), 400 + + start_time = datetime.now() + duration_minutes = exam.get('duration_minutes', 30) + end_time = datetime.fromtimestamp(start_time.timestamp() + (duration_minutes * 60)) + + result = db.exams.update_one( + {"exam_code": exam_code}, + { + "$set": { + "status": "active", + "start_time": start_time, + "end_time": end_time, + "updated_at": datetime.now() + } + } + ) + + if result.modified_count > 0: + return jsonify({ + "success": True, + "message": "Exam started successfully", + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat() + }) + else: + return jsonify({"success": False, "error": "Failed to start exam"}), 500 + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/exam/stop-exam', methods=['POST', 'OPTIONS']) +def stop_exam_direct(): + """Stop an 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() + + db = get_db() + result = db.exams.update_one( + {"exam_code": exam_code}, + { + "$set": { + "status": "completed", + "completed_at": datetime.now(), + "updated_at": datetime.now() + } + } + ) + + if result.modified_count > 0: + return jsonify({ + "success": True, + "message": "Exam stopped successfully" + }) + else: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + +# =================================================================== +# ✅ COMPILER ENDPOINT +# =================================================================== + +@app.route('/api/compiler/execute', methods=['POST', 'OPTIONS']) +def execute_code_direct(): + """Direct compiler endpoint""" + 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() + language = data.get('language', 'python').lower() + code = data.get('code', '').strip() + + if not code: + return jsonify({"success": False, "error": "No code provided"}), 400 + + if language == 'python': + try: + import io + from contextlib import redirect_stdout, redirect_stderr + import time + + stdout_buffer = io.StringIO() + stderr_buffer = io.StringIO() + + start_time = time.time() + + try: + with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): + exec(code, {"__builtins__": __builtins__}) + + execution_time = time.time() - start_time + stdout_content = stdout_buffer.getvalue() + stderr_content = stderr_buffer.getvalue() + + return jsonify({ + "success": True, + "output": stdout_content or "Code executed successfully (no output)", + "error": stderr_content if stderr_content else None, + "language": "python", + "execution_time": round(execution_time, 3) + }) + + except Exception as e: + execution_time = time.time() - start_time + return jsonify({ + "success": False, + "error": f"Runtime error: {str(e)}", + "execution_time": round(execution_time, 3) + }) + + except Exception as e: + return jsonify({"success": False, "error": f"Setup failed: {str(e)}"}), 500 + else: + return jsonify({ + "success": False, + "error": f"Language '{language}' not supported. Only Python available." + }) + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + +# =================================================================== +# ✅ DEBUG ENDPOINTS +# =================================================================== + +@app.route('/api/debug/complete-reset/', methods=['POST', 'OPTIONS']) +def complete_reset_exam(exam_code): + """COMPLETE RESET: Fix all participant-submission mismatches""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response + + try: + db = get_db() + exam_code = exam_code.upper() + + print(f"🔥 COMPLETE RESET for exam: {exam_code}") + + # 1. Get all submissions + submissions = list(db.submissions.find({"exam_code": exam_code})) + print(f"📋 Found {len(submissions)} submissions") + + # 2. DELETE ALL participants + delete_result = db.participants.delete_many({"exam_code": exam_code}) + print(f"🗑️ Deleted {delete_result.deleted_count} participant records") + + # 3. Recreate ONLY completed participants from submissions + created_count = 0 + for submission in submissions: + participant_name = submission.get('participant_name') + if not participant_name: + continue + + new_participant = { + "exam_code": exam_code, + "name": participant_name, + "completed": True, # ✅ ALWAYS TRUE for submissions + "score": submission.get('score', 0), + "submitted_at": submission.get('submitted_at'), + "joined_at": submission.get('submitted_at'), + "language": submission.get('language'), + "passed_tests": submission.get('passed_tests', 0), + "total_tests": submission.get('total_tests', 1), + "points_earned": submission.get('scoring_details', {}).get('points_earned', 0), + "total_points": submission.get('scoring_details', {}).get('total_points', 100), + "session_id": f"reset-{uuid.uuid4()}", + "rank": 0 + } + + db.participants.insert_one(new_participant) + print(f"✅ Created completed participant: {participant_name} with score {submission.get('score', 0)}%") + created_count += 1 + + print(f"🎯 COMPLETE RESET FINISHED: {created_count} participants recreated") + + return jsonify({ + "success": True, + "message": f"Complete reset finished for {exam_code}", + "submissions_found": len(submissions), + "participants_deleted": delete_result.deleted_count, + "participants_created": created_count + }) + + except Exception as e: + print(f"❌ Complete reset error: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + +# =================================================================== +# ✅ REQUEST HANDLERS +# =================================================================== + +# Request logging +@app.before_request +def log_request(): + path = request.path + if path.startswith('/api/exam'): + logger.info(f"📥 Exam request: {request.method} {path}") + +# Handle CORS preflight +@app.before_request +def handle_options(): + if request.method == 'OPTIONS': + resp = jsonify({'status':'ok'}) + resp.headers.update({ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Content-Type,Authorization,Accept,Origin,X-Requested-With", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Credentials": "true" + }) + return resp + +# Health endpoints +@app.route('/') +def health_root(): + return jsonify({ + "status":"OpenLearnX API running", + "version":"2.4.0 - ULTIMATE LEADERBOARD FIX", + "timestamp": datetime.now().isoformat(), + "features":{ + "mongodb": MONGO_SERVICE_AVAILABLE, + "web3": WEB3_SERVICE_AVAILABLE, + "wallet": WALLET_SERVICE_AVAILABLE, + "compiler": COMPILER_SERVICE_AVAILABLE, + "docker": check_docker_availability(), + "dynamic_scoring": True, + "ultimate_leaderboard_fix": True + } + }) + +@app.route('/api/health') +def api_health(): + status = "healthy" + services = { + "mongodb": MONGO_SERVICE_AVAILABLE, + "web3": WEB3_SERVICE_AVAILABLE, + "wallet": WALLET_SERVICE_AVAILABLE, + "compiler": COMPILER_SERVICE_AVAILABLE, + "docker": check_docker_availability(), + "ultimate_leaderboard_fix": True + } + + if MONGO_SERVICE_AVAILABLE: + try: + db = get_db() + db.command('ismaster') + services["mongodb_connection"] = "connected" + except Exception as e: + services["mongodb_connection"] = f"error: {str(e)}" + status = "degraded" + + return jsonify({ + "status": status, + "services": services, + "blueprints_registered": blueprints_registered, + "blueprints_failed": blueprints_failed + }), 200 if status == "healthy" else 503 + +# Error handlers +@app.errorhandler(404) +def not_found(e): + return jsonify({ + "error": "Not Found", + "path": request.path, + "method": request.method + }), 404 + +@app.errorhandler(500) +def internal_error(e): + logger.error(f"500 Error: {e}") + return jsonify({ + "error": "Internal Server Error", + "timestamp": datetime.now().isoformat() + }), 500 + +if __name__ == '__main__': + logger.info("🚀 Starting OpenLearnX Backend with ULTIMATE LEADERBOARD FIX") + logger.info("📚 Features: Dynamic Scoring, Duplicate Handling, Force Sync") + logger.info("🌐 Server starting on http://0.0.0.0:5000") + + app.run(host='0.0.0.0', port=5000, debug=True, threaded=True, use_reloader=False) diff --git a/backend/backend/models/user.py b/backend/backend/models/user.py new file mode 100644 index 0000000..5093fb4 --- /dev/null +++ b/backend/backend/models/user.py @@ -0,0 +1,30 @@ +from bson import ObjectId +from datetime import datetime +from pymongo.collection import Collection + +class UserModel: + def __init__(self, collection: Collection): + self.collection = collection + + async def get_by_wallet(self, wallet_address: str): + return await self.collection.find_one({"wallet_address": wallet_address.lower()}) + + async def create_user(self, wallet_address: str): + now = datetime.utcnow() + user = { + "wallet_address": wallet_address.lower(), + "created_at": now, + "last_login": now, + "total_tests": 0, + "certificates": [] + } + result = await self.collection.insert_one(user) + user["_id"] = result.inserted_id + return user + + async def update_last_login(self, wallet_address: str): + now = datetime.utcnow() + await self.collection.update_one( + {"wallet_address": wallet_address.lower()}, + {"$set": {"last_login": now}} + ) diff --git a/backend/backend/mongo_service.py b/backend/backend/mongo_service.py new file mode 100644 index 0000000..55b27ee --- /dev/null +++ b/backend/backend/mongo_service.py @@ -0,0 +1,142 @@ +from motor.motor_asyncio import AsyncIOMotorClient +from pymongo.errors import ServerSelectionTimeoutError +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Any + + +class MongoService: + def __init__(self, uri: str): + self.uri = uri # Store URI for sync operations + try: + # Simple connection without custom SSL context + self.client = AsyncIOMotorClient( + uri, + serverSelectionTimeoutMS=30000, + connectTimeoutMS=30000, + socketTimeoutMS=30000 + ) + print("MongoDB client initialized successfully") + except Exception as e: + print(f"MongoDB connection failed: {e}") + # Fallback to basic connection + self.client = AsyncIOMotorClient(uri) + + self.db = self.client.openlearnx + # Collections + self.users = self.db.users + self.questions = self.db.questions + self.test_sessions = self.db.test_sessions + self.certificates = self.db.certificates + self.peer_reviews = self.db.peer_reviews + + async def init_db(self): + """Initialize database with indexes and sample data""" + try: + # Test connection first + await self.client.admin.command('ping') + print("MongoDB connection successful!") + + # Create indexes + await self.users.create_index("wallet_address", unique=True) + await self.users.create_index("email", unique=True, sparse=True) + await self.questions.create_index("subject") + await self.questions.create_index("difficulty") + await self.test_sessions.create_index("user_id") + await self.test_sessions.create_index("created_at") + await self.certificates.create_index("user_id") + await self.certificates.create_index("token_id", unique=True) + + # Insert sample questions if none exist + if await self.questions.count_documents({}) == 0: + await self.insert_sample_questions() + print("Sample questions inserted successfully") + + except ServerSelectionTimeoutError as e: + print(f"Failed to connect to MongoDB: {e}") + print("Continuing without database initialization...") + except Exception as e: + print(f"Database initialization error: {e}") + print("Continuing without database initialization...") + + async def get_user_by_wallet(self, wallet_address: str): + """Get user by wallet address""" + return await self.users.find_one({"wallet_address": wallet_address.lower()}) + + async def create_user(self, wallet_address: str): + """Create a new user""" + now = datetime.utcnow() + user = { + "wallet_address": wallet_address.lower(), + "created_at": now, + "last_login": now, + "total_tests": 0, + "certificates": [] + } + result = await self.users.insert_one(user) + user["_id"] = result.inserted_id + return user + + async def update_user_login(self, wallet_address: str): + """Update user's last login time""" + await self.users.update_one( + {"wallet_address": wallet_address.lower()}, + {"$set": {"last_login": datetime.utcnow()}} + ) + + async def insert_sample_questions(self): + """Insert sample questions - implement based on your needs""" + # You'll need to implement this method based on your question structure + sample_questions = [ + { + "subject": "Python", + "difficulty": "beginner", + "question": "What is a variable in Python?", + "options": ["A storage location", "A function", "A loop", "A condition"], + "correct_answer": 0, + "created_at": datetime.utcnow() + }, + # Add more sample questions as needed + ] + await self.questions.insert_many(sample_questions) + + async def close_connection(self): + """Close the database connection""" + if self.client: + self.client.close() + print("MongoDB connection closed") + + def create_user_sync(self, wallet_address: str): + """Synchronous user creation using pymongo instead of motor""" + import pymongo + + # Create a synchronous connection for this operation only + client = pymongo.MongoClient(self.uri) + db = client.openlearnx + users = db.users + + try: + # Check if user exists + user = users.find_one({"wallet_address": wallet_address.lower()}) + + if not user: + # Create new user + new_user = { + "wallet_address": wallet_address.lower(), + "created_at": datetime.utcnow(), + "last_login": datetime.utcnow(), + "total_tests": 0, + "certificates": [] + } + result = users.insert_one(new_user) + new_user["_id"] = result.inserted_id + return new_user + else: + # Update last login + users.update_one( + {"wallet_address": wallet_address.lower()}, + {"$set": {"last_login": datetime.utcnow()}} + ) + return user + finally: + # Always close the connection + client.close() \ No newline at end of file diff --git a/backend/backend/routes/admin.py b/backend/backend/routes/admin.py new file mode 100644 index 0000000..a69d074 --- /dev/null +++ b/backend/backend/routes/admin.py @@ -0,0 +1,428 @@ +from flask import Blueprint, request, jsonify +from functools import wraps +import uuid +from datetime import datetime +from pymongo import MongoClient +import os +from bson import ObjectId + +bp = Blueprint('admin', __name__) + +# MongoDB connection +mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') +client = MongoClient(mongo_uri) +db = client.openlearnx + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + try: + auth_header = request.headers.get('Authorization') + print(f"Admin auth check - Header: {auth_header}") + + if not auth_header: + print("❌ No Authorization header") + return jsonify({"error": "No authorization header provided"}), 401 + + if not auth_header.startswith('Bearer '): + print("❌ Invalid authorization format") + return jsonify({"error": "Invalid authorization format"}), 401 + + token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None + print(f"Extracted token: '{token}'") + + # Check environment variable first, then fallback to default + expected_token = os.getenv('ADMIN_TOKEN') + if not expected_token: + expected_token = 'admin-secret-key' + + print(f"Expected token: '{expected_token}'") + print(f"Environment ADMIN_TOKEN: '{os.getenv('ADMIN_TOKEN')}'") + + # Strip any whitespace from both tokens + if token and expected_token: + if token.strip() == expected_token.strip(): + print("✅ Admin authentication successful") + return f(*args, **kwargs) + + print("❌ Token mismatch") + return jsonify({"error": "Invalid admin token"}), 401 + + except Exception as e: + print(f"❌ Admin auth error: {str(e)}") + return jsonify({"error": "Authentication failed"}), 500 + + return decorated_function + +def serialize_course(course): + """Convert MongoDB document to JSON-serializable format""" + if course: + if '_id' in course: + del course['_id'] + return course + return None + +def convert_to_embed_url(youtube_url): + """Convert YouTube watch URL to embed URL - ENHANCED VERSION""" + if not youtube_url: + return None + + try: + if "youtu.be/" in youtube_url: + video_id = youtube_url.split("youtu.be/")[1].split("?")[0].split("&")[0] + elif "youtube.com/watch?v=" in youtube_url: + video_id = youtube_url.split("v=")[1].split("&")[0] + elif "youtube.com/embed/" in youtube_url: + return youtube_url + else: + return None + + video_id = video_id.strip() + return f"https://www.youtube.com/embed/{video_id}?rel=0&modestbranding=1" + except Exception as e: + print(f"Error converting YouTube URL: {e}") + return None + +@bp.route("/test", methods=["GET"]) +@admin_required +def test_admin(): + """Test admin authentication""" + return jsonify({ + "success": True, + "message": "Admin authentication working", + "timestamp": datetime.now().isoformat() + }) + +@bp.route("/dashboard", methods=["GET"]) +@admin_required +def admin_dashboard(): + """Get admin dashboard statistics""" + try: + total_courses = db.courses.count_documents({}) + total_lessons = db.lessons.count_documents({}) + active_students = db.users.count_documents({"status": "active"}) or 2341 + + stats = { + "total_courses": total_courses, + "total_lessons": total_lessons, + "active_students": active_students, + "completion_rate": 78 + } + return jsonify(stats) + except Exception as e: + print(f"Dashboard error: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses", methods=["GET"]) +@admin_required +def get_admin_courses(): + """Get all courses for admin management""" + try: + print("Fetching courses from database...") + courses = list(db.courses.find({}, {"_id": 0})) + print(f"Found {len(courses)} courses") + + for course in courses: + course["students"] = course.get("students", 0) + course["status"] = "published" + + return jsonify(courses) + except Exception as e: + print(f"Error fetching courses: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses", methods=["POST"]) +@admin_required +def create_course(): + """Create new course""" + try: + data = request.json + print(f"Creating course with data: {data}") # Debug log + + course_id = data.get('id') or f"{data.get('title', '').lower().replace(' ', '-').replace('&', 'and')}-course" + + existing_course = db.courses.find_one({"id": course_id}) + if existing_course: + return jsonify({"error": "Course with this ID already exists"}), 400 + + new_course = { + "id": course_id, + "title": data.get('title'), + "subject": data.get('subject'), + "description": data.get('description'), + "difficulty": data.get('difficulty'), + "mentor": data.get('mentor', '5t4l1n'), + "video_url": data.get('video_url'), + "embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None, + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 0, + "progress": 0, + "modules": [] + } + + result = db.courses.insert_one(new_course) + print(f"Course created with ID: {result.inserted_id}") + + # Remove _id field before returning + new_course_response = serialize_course(new_course) + + return jsonify({"success": True, "course": new_course_response}), 201 + + except Exception as e: + print(f"Error creating course: {e}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses/", methods=["PUT"]) +@admin_required +def update_course(course_id): + """Update existing course - FIXED VERSION""" + try: + data = request.json + print(f"Updating course {course_id} with data: {data}") # Debug log + + update_data = { + "title": data.get('title'), + "subject": data.get('subject'), + "description": data.get('description'), + "difficulty": data.get('difficulty'), + "mentor": data.get('mentor'), + "video_url": data.get('video_url'), + "embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None, + "updated_at": datetime.now().isoformat() + } + + # Remove None values + update_data = {k: v for k, v in update_data.items() if v is not None} + print(f"Filtered update data: {update_data}") # Debug log + + result = db.courses.update_one( + {"id": course_id}, + {"$set": update_data} + ) + + print(f"Update result: matched={result.matched_count}, modified={result.modified_count}") # Debug log + + if result.matched_count == 0: + return jsonify({"error": "Course not found"}), 404 + + # Get updated course without _id field + updated_course = db.courses.find_one({"id": course_id}, {"_id": 0}) + return jsonify({"success": True, "course": updated_course}) + + except Exception as e: + print(f"Error updating course: {e}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses/", methods=["DELETE"]) +@admin_required +def delete_course(course_id): + """Delete course""" + try: + print(f"Deleting course: {course_id}") # Debug log + + result = db.courses.delete_one({"id": course_id}) + + if result.deleted_count == 0: + return jsonify({"error": "Course not found"}), 404 + + # Also delete related lessons + lesson_result = db.lessons.delete_many({"course_id": course_id}) + print(f"Deleted {lesson_result.deleted_count} related lessons") # Debug log + + return jsonify({"success": True, "message": "Course deleted successfully"}) + + except Exception as e: + print(f"Error deleting course: {e}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses//modules", methods=["POST"]) +@admin_required +def add_module(course_id): + """Add module to course""" + try: + data = request.json + + module = { + "id": data.get('id') or str(uuid.uuid4()), + "title": data.get('title'), + "lessons": [] + } + + result = db.courses.update_one( + {"id": course_id}, + {"$push": {"modules": module}} + ) + + if result.matched_count == 0: + return jsonify({"error": "Course not found"}), 404 + + return jsonify({"success": True, "module": module}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/courses//lessons", methods=["POST"]) +@admin_required +def add_lesson(course_id): + """Add lesson to course""" + try: + data = request.json + + lesson = { + "id": data.get('id') or str(uuid.uuid4()), + "course_id": course_id, + "title": data.get('title'), + "type": data.get('type', 'video'), + "duration": data.get('duration'), + "description": data.get('description'), + "content": data.get('content'), + "video_url": data.get('video_url'), + "embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None, + "created_at": datetime.now().isoformat() + } + + # Insert lesson + db.lessons.insert_one(lesson) + + # Remove _id field before returning + lesson_response = serialize_course(lesson) + + return jsonify({"success": True, "lesson": lesson_response}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/initialize", methods=["POST"]) +@admin_required +def initialize_default_courses(): + """Initialize database with default courses""" + try: + existing_count = db.courses.count_documents({}) + if existing_count > 0: + return jsonify({"message": f"Courses already initialized ({existing_count} courses found)"}), 200 + + default_courses = [ + { + "id": "python-course", + "title": "Python Programming Mastery", + "subject": "Programming", + "description": "Learn Python from basics to advanced concepts including turtle graphics", + "difficulty": "Beginner to Advanced", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp", + "embed_url": "https://www.youtube.com/embed/SsH8GJlqUIg?rel=0&modestbranding=1", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 1250, + "progress": 0, + "modules": [] + }, + { + "id": "java-course", + "title": "Java Development Bootcamp", + "subject": "Programming", + "description": "Master Java programming with object-oriented concepts", + "difficulty": "Intermediate", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp", + "embed_url": "https://www.youtube.com/embed/SsH8GJlqUIg?rel=0&modestbranding=1", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 890, + "progress": 0, + "modules": [] + }, + { + "id": "ethical-hacking-course", + "title": "Ethical Hacking & Cybersecurity", + "subject": "Cybersecurity", + "description": "Learn ethical hacking techniques and penetration testing", + "difficulty": "Advanced", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS", + "embed_url": "https://www.youtube.com/embed/cDnX0vyNTaE?rel=0&modestbranding=1", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 567, + "progress": 0, + "modules": [] + }, + { + "id": "dark-web-hosting-course", + "title": "Learn Dark Web Hosting", + "subject": "Cybersecurity", + "description": "Understanding dark web infrastructure, Tor networks, and secure hosting practices for cybersecurity professionals", + "difficulty": "Expert", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U", + "embed_url": "https://www.youtube.com/embed/Z4_USAMVhYs?rel=0&modestbranding=1", + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "students": 234, + "progress": 0, + "modules": [] + } + ] + + result = db.courses.insert_many(default_courses) + print(f"Initialized {len(result.inserted_ids)} default courses") + + return jsonify({ + "success": True, + "message": f"Default courses initialized successfully", + "courses_created": len(result.inserted_ids) + }) + except Exception as e: + print(f"Error initializing courses: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/stats", methods=["GET"]) +@admin_required +def get_admin_stats(): + """Get detailed admin statistics""" + try: + total_courses = db.courses.count_documents({}) + total_lessons = db.lessons.count_documents({}) + + # Course statistics by subject + pipeline = [ + {"$group": {"_id": "$subject", "count": {"$sum": 1}}} + ] + subjects = list(db.courses.aggregate(pipeline)) + + # Course statistics by difficulty + pipeline = [ + {"$group": {"_id": "$difficulty", "count": {"$sum": 1}}} + ] + difficulties = list(db.courses.aggregate(pipeline)) + + stats = { + "total_courses": total_courses, + "total_lessons": total_lessons, + "subjects": subjects, + "difficulties": difficulties, + "last_updated": datetime.now().isoformat() + } + + return jsonify(stats) + except Exception as e: + print(f"Error getting stats: {str(e)}") + return jsonify({"error": str(e)}), 500 + +@bp.route("/health", methods=["GET"]) +def admin_health(): + """Admin health check endpoint""" + return jsonify({ + "status": "Admin API is healthy", + "timestamp": datetime.now().isoformat(), + "database_connected": True, + "endpoints": [ + "GET /api/admin/dashboard", + "GET /api/admin/courses", + "POST /api/admin/courses", + "PUT /api/admin/courses/", + "DELETE /api/admin/courses/", + "POST /api/admin/initialize", + "GET /api/admin/test", + "GET /api/admin/stats" + ] + }) diff --git a/backend/backend/routes/auth.py b/backend/backend/routes/auth.py new file mode 100644 index 0000000..ab51788 --- /dev/null +++ b/backend/backend/routes/auth.py @@ -0,0 +1,90 @@ +from flask import Blueprint, request, jsonify, current_app +import jwt +from datetime import datetime, timedelta +import secrets + +bp = Blueprint("auth", __name__) + +# Store nonces temporarily (in production, use Redis or database) +nonces = {} + +@bp.route("/nonce", methods=["POST"]) +def get_nonce(): + data = request.get_json() + wallet_address = data.get("wallet_address") + + if not wallet_address: + return jsonify({"error": "wallet_address is required"}), 400 + + # Generate nonce + nonce = secrets.token_hex(16) + message = f"Sign this message to authenticate with OpenLearnX: {nonce}" + + # Store nonce for this wallet address + nonces[wallet_address.lower()] = nonce + + return jsonify({"nonce": nonce, "message": message}) + +@bp.route("/verify", methods=["POST"]) +def verify_signature(): + data = request.get_json() + wallet_address = data.get("wallet_address", "").lower() + signature = data.get("signature") + message = data.get("message") + + if not all([wallet_address, signature, message]): + return jsonify({"error": "Missing required fields"}), 400 + + # Verify nonce + stored_nonce = nonces.get(wallet_address) + if not stored_nonce or stored_nonce not in message: + return jsonify({"error": "Invalid nonce"}), 400 + + try: + web3_service = current_app.config["WEB3_SERVICE"] + + # Verify signature + if not web3_service.verify_signature(wallet_address, message, signature): + return jsonify({"error": "Invalid signature"}), 401 + + # For now, create a mock user without database operations + # This bypasses the async MongoDB issues entirely + user = { + "_id": f"user_{wallet_address}", + "wallet_address": wallet_address, + "created_at": datetime.utcnow(), + "total_tests": 0, + "certificates": [] + } + + # Create JWT token + token_payload = { + "user_id": str(user["_id"]), + "wallet_address": wallet_address, + "exp": datetime.utcnow() + timedelta(days=7) + } + + token = jwt.encode( + token_payload, + current_app.config["SECRET_KEY"], + algorithm="HS256" + ) + + # Clean up nonce + if wallet_address in nonces: + del nonces[wallet_address] + + return jsonify({ + "success": True, + "token": token, + "user": { + "id": str(user["_id"]), + "wallet_address": user["wallet_address"], + "total_tests": user.get("total_tests", 0), + "certificates": len(user.get("certificates", [])) + } + }) + + except Exception as e: + print(f"Authentication error: {str(e)}") + return jsonify({"error": "Authentication failed"}), 500 diff --git a/backend/backend/routes/certificate.py b/backend/backend/routes/certificate.py new file mode 100644 index 0000000..497b63e --- /dev/null +++ b/backend/backend/routes/certificate.py @@ -0,0 +1,49 @@ +from flask import Blueprint, request, jsonify, current_app +import jwt + +bp = Blueprint('certificate', __name__) + +def get_user_from_token(token): + """Extract user from JWT token""" + try: + payload = jwt.decode( + token, + current_app.config['SECRET_KEY'], + algorithms=['HS256'] + ) + return payload['user_id'], payload['wallet_address'] + except: + return None, None + +@bp.route('/user/', methods=['GET']) +async def get_user_certificates(user_id): + """Get all certificates for a user""" + token = request.headers.get('Authorization', '').replace('Bearer ', '') + token_user_id, _ = get_user_from_token(token) + + if not token_user_id or token_user_id != user_id: + return jsonify({"error": "Unauthorized"}), 403 + + mongo_service = current_app.config['MONGO_SERVICE'] + certificates = await mongo_service.get_user_certificates(user_id) + + return jsonify({"certificates": certificates or []}) + +@bp.route('/mint', methods=['POST']) +async def mint_certificate(): + """Mint NFT certificate for completed test""" + token = request.headers.get('Authorization', '').replace('Bearer ', '') + user_id, wallet_address = get_user_from_token(token) + + if not user_id: + return jsonify({"error": "Authentication required"}), 401 + + # Mock certificate minting for now + return jsonify({ + "success": True, + "certificate": { + "token_id": 1, + "transaction_hash": "0x123...", + "message": "Certificate minting functionality ready" + } + }) diff --git a/backend/backend/routes/coding.py b/backend/backend/routes/coding.py new file mode 100644 index 0000000..5b04bb9 --- /dev/null +++ b/backend/backend/routes/coding.py @@ -0,0 +1,240 @@ +from flask import Blueprint, request, jsonify, session +from functools import wraps +import subprocess +import tempfile +import os +import time +import uuid +from datetime import datetime +import docker +import psutil + +bp = Blueprint('coding', __name__) + +def secure_execution_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + # Check if user is in secure coding mode + if not session.get('secure_coding_mode'): + return jsonify({"error": "Secure coding mode required"}), 403 + return f(*args, **kwargs) + return decorated_function + +@bp.route("/start-session", methods=["POST"]) +def start_coding_session(): + """Start a secure coding session""" + try: + data = request.json + course_id = data.get('course_id') + lesson_id = data.get('lesson_id') + + session_id = str(uuid.uuid4()) + session['coding_session_id'] = session_id + session['secure_coding_mode'] = True + session['start_time'] = datetime.now().isoformat() + session['course_id'] = course_id + session['lesson_id'] = lesson_id + + return jsonify({ + "success": True, + "session_id": session_id, + "message": "Secure coding session started", + "restrictions": { + "copy_paste_disabled": True, + "browser_locked": True, + "extensions_blocked": True, + "virtual_detection": True + } + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/execute", methods=["POST"]) +@secure_execution_required +def execute_code(): + """Execute code securely in isolated environment""" + try: + data = request.json + code = data.get('code') + language = data.get('language', 'python') + test_cases = data.get('test_cases', []) + + if not code: + return jsonify({"error": "No code provided"}), 400 + + # Log coding attempt + log_coding_attempt(session['coding_session_id'], code, language) + + # Execute code in secure container + result = execute_in_container(code, language, test_cases) + + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("/submit-test", methods=["POST"]) +@secure_execution_required +def submit_coding_test(): + """Submit coding test for evaluation""" + try: + data = request.json + code = data.get('code') + problem_id = data.get('problem_id') + + # Validate against test cases + test_result = validate_test_submission(code, problem_id) + + # Store submission + submission_id = store_submission( + session['coding_session_id'], + session['course_id'], + problem_id, + code, + test_result + ) + + return jsonify({ + "success": True, + "submission_id": submission_id, + "score": test_result['score'], + "passed_tests": test_result['passed'], + "total_tests": test_result['total'], + "feedback": test_result['feedback'] + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +def execute_in_container(code, language, test_cases): + """Execute code in secure Docker container""" + try: + client = docker.from_env() + + # Language-specific container configuration + containers = { + 'python': 'python:3.9-alpine', + 'java': 'openjdk:11-alpine', + 'javascript': 'node:16-alpine' + } + + if language not in containers: + return {"error": "Unsupported language"} + + # Create temporary file + with tempfile.NamedTemporaryFile(mode='w', suffix=f'.{get_file_extension(language)}', delete=False) as f: + f.write(code) + temp_file = f.name + + try: + # Run container with security restrictions + container = client.containers.run( + containers[language], + command=get_run_command(language, temp_file), + volumes={os.path.dirname(temp_file): {'bind': '/app', 'mode': 'ro'}}, + working_dir='/app', + mem_limit='128m', + cpu_period=100000, + cpu_quota=50000, # 50% CPU limit + network_mode='none', # No network access + remove=True, + timeout=10, # 10 second timeout + detach=False + ) + + output = container.decode('utf-8') + + # Run test cases if provided + test_results = [] + if test_cases: + for test in test_cases: + test_result = run_test_case(code, language, test) + test_results.append(test_result) + + return { + "success": True, + "output": output, + "test_results": test_results, + "execution_time": "< 10s" + } + + finally: + os.unlink(temp_file) + + except docker.errors.ContainerError as e: + return {"error": f"Runtime error: {e}"} + except docker.errors.ImageNotFound: + return {"error": "Language runtime not available"} + except Exception as e: + return {"error": f"Execution failed: {str(e)}"} + +def get_file_extension(language): + extensions = { + 'python': 'py', + 'java': 'java', + 'javascript': 'js' + } + return extensions.get(language, 'txt') + +def get_run_command(language, filename): + commands = { + 'python': f'python /app/{os.path.basename(filename)}', + 'java': f'javac /app/{os.path.basename(filename)} && java -cp /app {os.path.splitext(os.path.basename(filename))[0]}', + 'javascript': f'node /app/{os.path.basename(filename)}' + } + return commands.get(language) + +def log_coding_attempt(session_id, code, language): + """Log all coding attempts for monitoring""" + from pymongo import MongoClient + + client = MongoClient(os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')) + db = client.openlearnx + + db.coding_logs.insert_one({ + "session_id": session_id, + "code": code, + "language": language, + "timestamp": datetime.now(), + "ip_address": request.remote_addr, + "user_agent": request.headers.get('User-Agent') + }) + +def validate_test_submission(code, problem_id): + """Validate code against predefined test cases""" + # Load test cases for the problem + test_cases = get_problem_test_cases(problem_id) + + passed = 0 + total = len(test_cases) + feedback = [] + + for i, test_case in enumerate(test_cases): + result = run_test_case(code, 'python', test_case) + if result['passed']: + passed += 1 + feedback.append(f"Test {i+1}: ✅ Passed") + else: + feedback.append(f"Test {i+1}: ❌ Failed - {result['error']}") + + score = (passed / total) * 100 + + return { + "score": score, + "passed": passed, + "total": total, + "feedback": feedback + } + +def get_problem_test_cases(problem_id): + """Get test cases for a specific problem""" + # This would load from your database + test_cases_db = { + "python-basics-1": [ + {"input": "hello", "expected_output": "HELLO"}, + {"input": "world", "expected_output": "WORLD"} + ], + "java-oop-1": [ + {"input": "5", "expected_output": "25"}, + {"input": "10", "expected_output": "100"} + ] + } + return test_cases_db.get(problem_id, []) diff --git a/backend/backend/routes/compiler.py b/backend/backend/routes/compiler.py new file mode 100644 index 0000000..02d436c --- /dev/null +++ b/backend/backend/routes/compiler.py @@ -0,0 +1,546 @@ +from flask import Blueprint, request, jsonify +import subprocess +import tempfile +import os +import time +import docker +from datetime import datetime + +bp = Blueprint('compiler', __name__) + +def get_db(): + """Get MongoDB database connection""" + from pymongo import MongoClient + from flask import current_app + client = MongoClient(current_app.config['MONGODB_URI']) + return client.openlearnx + +@bp.route('/execute', methods=['POST', 'OPTIONS']) +def execute_code(): + """Execute code in specified language with Docker support""" + 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() + language = data.get('language', 'python').lower() + code = data.get('code', '').strip() + input_data = data.get('input', '') + + print(f"🔧 Executing {language} code") + print(f"📝 Code length: {len(code)} characters") + + if not code: + return jsonify({"success": False, "error": "No code provided"}), 400 + + # Execute based on language + if language == 'python': + return execute_python(code, input_data) + elif language == 'java': + return execute_java(code, input_data) + elif language == 'javascript' or language == 'js': + return execute_javascript(code, input_data) + elif language == 'cpp' or language == 'c++': + return execute_cpp(code, input_data) + elif language == 'c': + return execute_c(code, input_data) + else: + return jsonify({ + "success": False, + "error": f"Language '{language}' not supported. Available: python, java, javascript, cpp, c" + }), 400 + + except Exception as e: + print(f"❌ Compiler error: {str(e)}") + import traceback + traceback.print_exc() + return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500 + +def execute_python(code, input_data=""): + """Execute Python code""" + try: + # Create temporary file + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write(code) + temp_file = f.name + + try: + # Execute with subprocess + start_time = time.time() + result = subprocess.run( + ['python3', temp_file], + input=input_data, + text=True, + capture_output=True, + timeout=10, # 10 second timeout + cwd=tempfile.gettempdir() + ) + execution_time = time.time() - start_time + + if result.returncode == 0: + return jsonify({ + "success": True, + "output": result.stdout or "Code executed successfully (no output)", + "error": result.stderr if result.stderr else None, + "language": "python", + "execution_time": round(execution_time, 3) + }) + else: + return jsonify({ + "success": False, + "error": result.stderr or f"Process exited with code {result.returncode}", + "language": "python" + }) + + finally: + # Clean up temp file + try: + os.unlink(temp_file) + except: + pass + + except subprocess.TimeoutExpired: + return jsonify({ + "success": False, + "error": "Code execution timed out (10s limit)" + }), 400 + except FileNotFoundError: + return jsonify({ + "success": False, + "error": "Python interpreter not found. Please install Python 3." + }), 500 + except Exception as e: + return jsonify({ + "success": False, + "error": f"Python execution error: {str(e)}" + }), 500 + +def execute_java(code, input_data=""): + """Execute Java code""" + try: + # Extract class name from code + import re + class_match = re.search(r'public\s+class\s+(\w+)', code) + if not class_match: + return jsonify({ + "success": False, + "error": "No public class found. Java code must contain 'public class ClassName'" + }), 400 + + class_name = class_match.group(1) + + # Create temporary directory + temp_dir = tempfile.mkdtemp() + java_file = os.path.join(temp_dir, f"{class_name}.java") + + try: + # Write Java code to file + with open(java_file, 'w') as f: + f.write(code) + + # Compile Java code + compile_result = subprocess.run( + ['javac', java_file], + capture_output=True, + text=True, + timeout=30, + cwd=temp_dir + ) + + if compile_result.returncode != 0: + return jsonify({ + "success": False, + "error": f"Compilation error:\n{compile_result.stderr}", + "language": "java" + }) + + # Execute Java code + start_time = time.time() + result = subprocess.run( + ['java', class_name], + input=input_data, + text=True, + capture_output=True, + timeout=10, + cwd=temp_dir + ) + execution_time = time.time() - start_time + + if result.returncode == 0: + return jsonify({ + "success": True, + "output": result.stdout or "Code executed successfully (no output)", + "error": result.stderr if result.stderr else None, + "language": "java", + "execution_time": round(execution_time, 3) + }) + else: + return jsonify({ + "success": False, + "error": result.stderr or f"Runtime error (exit code {result.returncode})", + "language": "java" + }) + + finally: + # Clean up temp files + import shutil + try: + shutil.rmtree(temp_dir) + except: + pass + + except subprocess.TimeoutExpired: + return jsonify({ + "success": False, + "error": "Code execution timed out" + }), 400 + except FileNotFoundError: + return jsonify({ + "success": False, + "error": "Java compiler/runtime not found. Please install JDK." + }), 500 + except Exception as e: + return jsonify({ + "success": False, + "error": f"Java execution error: {str(e)}" + }), 500 + +def execute_javascript(code, input_data=""): + """Execute JavaScript code""" + try: + # Create temporary file + with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f: + # Add input handling if needed + if input_data: + js_code = f""" +const input = `{input_data}`; +const readline = {{ question: () => input }}; +{code} +""" + else: + js_code = code + + f.write(js_code) + temp_file = f.name + + try: + # Execute with Node.js + start_time = time.time() + result = subprocess.run( + ['node', temp_file], + input=input_data, + text=True, + capture_output=True, + timeout=10, + cwd=tempfile.gettempdir() + ) + execution_time = time.time() - start_time + + if result.returncode == 0: + return jsonify({ + "success": True, + "output": result.stdout or "Code executed successfully (no output)", + "error": result.stderr if result.stderr else None, + "language": "javascript", + "execution_time": round(execution_time, 3) + }) + else: + return jsonify({ + "success": False, + "error": result.stderr or f"Runtime error (exit code {result.returncode})", + "language": "javascript" + }) + + finally: + try: + os.unlink(temp_file) + except: + pass + + except subprocess.TimeoutExpired: + return jsonify({ + "success": False, + "error": "Code execution timed out" + }), 400 + except FileNotFoundError: + return jsonify({ + "success": False, + "error": "Node.js not found. Please install Node.js." + }), 500 + except Exception as e: + return jsonify({ + "success": False, + "error": f"JavaScript execution error: {str(e)}" + }), 500 + +def execute_cpp(code, input_data=""): + """Execute C++ code""" + try: + # Create temporary files + temp_dir = tempfile.mkdtemp() + cpp_file = os.path.join(temp_dir, "main.cpp") + exe_file = os.path.join(temp_dir, "main.exe") if os.name == 'nt' else os.path.join(temp_dir, "main") + + try: + # Write C++ code to file + with open(cpp_file, 'w') as f: + f.write(code) + + # Compile C++ code + compile_cmd = ['g++', '-o', exe_file, cpp_file, '-std=c++17'] + compile_result = subprocess.run( + compile_cmd, + capture_output=True, + text=True, + timeout=30, + cwd=temp_dir + ) + + if compile_result.returncode != 0: + return jsonify({ + "success": False, + "error": f"Compilation error:\n{compile_result.stderr}", + "language": "cpp" + }) + + # Execute compiled program + start_time = time.time() + result = subprocess.run( + [exe_file], + input=input_data, + text=True, + capture_output=True, + timeout=10, + cwd=temp_dir + ) + execution_time = time.time() - start_time + + if result.returncode == 0: + return jsonify({ + "success": True, + "output": result.stdout or "Code executed successfully (no output)", + "error": result.stderr if result.stderr else None, + "language": "cpp", + "execution_time": round(execution_time, 3) + }) + else: + return jsonify({ + "success": False, + "error": result.stderr or f"Runtime error (exit code {result.returncode})", + "language": "cpp" + }) + + finally: + # Clean up temp files + import shutil + try: + shutil.rmtree(temp_dir) + except: + pass + + except subprocess.TimeoutExpired: + return jsonify({ + "success": False, + "error": "Code execution timed out" + }), 400 + except FileNotFoundError: + return jsonify({ + "success": False, + "error": "G++ compiler not found. Please install GCC/G++." + }), 500 + except Exception as e: + return jsonify({ + "success": False, + "error": f"C++ execution error: {str(e)}" + }), 500 + +def execute_c(code, input_data=""): + """Execute C code""" + try: + # Create temporary files + temp_dir = tempfile.mkdtemp() + c_file = os.path.join(temp_dir, "main.c") + exe_file = os.path.join(temp_dir, "main.exe") if os.name == 'nt' else os.path.join(temp_dir, "main") + + try: + # Write C code to file + with open(c_file, 'w') as f: + f.write(code) + + # Compile C code + compile_cmd = ['gcc', '-o', exe_file, c_file, '-std=c99'] + compile_result = subprocess.run( + compile_cmd, + capture_output=True, + text=True, + timeout=30, + cwd=temp_dir + ) + + if compile_result.returncode != 0: + return jsonify({ + "success": False, + "error": f"Compilation error:\n{compile_result.stderr}", + "language": "c" + }) + + # Execute compiled program + start_time = time.time() + result = subprocess.run( + [exe_file], + input=input_data, + text=True, + capture_output=True, + timeout=10, + cwd=temp_dir + ) + execution_time = time.time() - start_time + + if result.returncode == 0: + return jsonify({ + "success": True, + "output": result.stdout or "Code executed successfully (no output)", + "error": result.stderr if result.stderr else None, + "language": "c", + "execution_time": round(execution_time, 3) + }) + else: + return jsonify({ + "success": False, + "error": result.stderr or f"Runtime error (exit code {result.returncode})", + "language": "c" + }) + + finally: + # Clean up temp files + import shutil + try: + shutil.rmtree(temp_dir) + except: + pass + + except subprocess.TimeoutExpired: + return jsonify({ + "success": False, + "error": "Code execution timed out" + }), 400 + except FileNotFoundError: + return jsonify({ + "success": False, + "error": "GCC compiler not found. Please install GCC." + }), 500 + except Exception as e: + return jsonify({ + "success": False, + "error": f"C execution error: {str(e)}" + }), 500 + +@bp.route('/languages', methods=['GET', 'OPTIONS']) +def get_supported_languages(): + """Get list of supported programming languages""" + 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: + languages = { + "python": { + "name": "Python", + "version": "3.x", + "extension": ".py", + "available": check_language_availability("python3") + }, + "java": { + "name": "Java", + "version": "JDK 8+", + "extension": ".java", + "available": check_language_availability("javac") + }, + "javascript": { + "name": "JavaScript", + "version": "Node.js", + "extension": ".js", + "available": check_language_availability("node") + }, + "cpp": { + "name": "C++", + "version": "GCC/G++", + "extension": ".cpp", + "available": check_language_availability("g++") + }, + "c": { + "name": "C", + "version": "GCC", + "extension": ".c", + "available": check_language_availability("gcc") + } + } + + return jsonify({ + "success": True, + "languages": languages, + "total": len(languages), + "available_count": sum(1 for lang in languages.values() if lang["available"]) + }) + + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + +def check_language_availability(command): + """Check if a language compiler/interpreter is available""" + try: + result = subprocess.run([command, '--version'], + capture_output=True, + timeout=5) + return result.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + +@bp.route('/health', methods=['GET']) +def compiler_health(): + """Health check for compiler service""" + try: + languages_status = { + "python": check_language_availability("python3"), + "java": check_language_availability("javac"), + "javascript": check_language_availability("node"), + "cpp": check_language_availability("g++"), + "c": check_language_availability("gcc") + } + + available_languages = sum(languages_status.values()) + total_languages = len(languages_status) + + status = "healthy" if available_languages > 0 else "unavailable" + + return jsonify({ + "status": status, + "timestamp": datetime.now().isoformat(), + "languages": languages_status, + "available_languages": available_languages, + "total_languages": total_languages, + "docker_available": check_docker_availability() + }) + + except Exception as e: + return jsonify({ + "status": "error", + "error": str(e), + "timestamp": datetime.now().isoformat() + }), 500 + +def check_docker_availability(): + """Check if Docker is available for containerized execution""" + try: + client = docker.from_env() + client.ping() + return True + except: + return False diff --git a/backend/backend/routes/courses.py b/backend/backend/routes/courses.py new file mode 100644 index 0000000..4e8cad6 --- /dev/null +++ b/backend/backend/routes/courses.py @@ -0,0 +1,93 @@ +from flask import Blueprint, jsonify, current_app +from pymongo import MongoClient +import os + +bp = Blueprint('courses', __name__) + +# MongoDB connection +mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') +client = MongoClient(mongo_uri) +db = client.openlearnx + +@bp.route("/", methods=["GET"]) +@bp.route("", methods=["GET"]) +def list_courses(): + """Get all courses - DYNAMIC from database""" + try: + courses = list(db.courses.find({}, {"_id": 0})) + + course_list = [] + for course in courses: + 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"), + "progress": course.get("progress", 0) + } + course_list.append(course_data) + + return jsonify(course_list) + except Exception as e: + print(f"Error in list_courses: {e}") + return jsonify({"error": "Failed to fetch courses"}), 500 + +@bp.route("/", methods=["GET"]) +def get_course(course_id): + """Get specific course details - DYNAMIC""" + try: + course = db.courses.find_one({"id": course_id}, {"_id": 0}) + + if not course: + return jsonify({"error": "Course not found"}), 404 + + return jsonify(course) + except Exception as e: + print(f"Error in get_course: {e}") + return jsonify({"error": "Failed to fetch course"}), 500 + +@bp.route("//lessons/", 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}) + + if not lesson: + return jsonify({"error": "Lesson not found"}), 404 + + return jsonify(lesson) + except Exception as e: + print(f"Error in get_lesson: {e}") + return jsonify({"error": "Failed to fetch lesson"}), 500 + +@bp.route("//lessons//complete", methods=["POST"]) +def mark_lesson_complete(course_id, lesson_id): + """Mark a lesson as completed for the user""" + try: + return jsonify({ + "success": True, + "message": f"Lesson {lesson_id} marked as complete", + "progress_updated": True + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("//progress", methods=["GET"]) +def get_course_progress(course_id): + """Get user's progress in a specific course""" + try: + progress = { + "course_id": course_id, + "completion_percentage": 25, + "lessons_completed": [], + "total_lessons": 4, + "last_accessed": "2025-01-26T23:30:00Z", + "time_spent": "2 hours 15 minutes" + } + return jsonify(progress) + except Exception as e: + return jsonify({"error": str(e)}), 500 diff --git a/backend/backend/routes/dashboard.py b/backend/backend/routes/dashboard.py new file mode 100644 index 0000000..588574c --- /dev/null +++ b/backend/backend/routes/dashboard.py @@ -0,0 +1,40 @@ +from flask import Blueprint, request, jsonify, current_app +import jwt + +bp = Blueprint('dashboard', __name__) + +def get_user_from_token(token): + """Extract user from JWT token""" + try: + payload = jwt.decode( + token, + current_app.config['SECRET_KEY'], + algorithms=['HS256'] + ) + return payload['user_id'] + except: + return None + +@bp.route('/student/', methods=['GET']) +async def get_student_dashboard(user_id): + """Get comprehensive student dashboard""" + token = request.headers.get('Authorization', '').replace('Bearer ', '') + token_user_id = get_user_from_token(token) + + if not token_user_id or token_user_id != user_id: + return jsonify({"error": "Unauthorized"}), 403 + + mongo_service = current_app.config['MONGO_SERVICE'] + analytics = await mongo_service.get_user_analytics(user_id) + + return jsonify(analytics or { + "user_info": {"id": user_id}, + "overview": { + "total_tests": 0, + "completed_tests": 0, + "average_score": 0, + "certificates_earned": 0 + }, + "subject_breakdown": {}, + "recent_activity": [] + }) diff --git a/backend/backend/routes/exam.py b/backend/backend/routes/exam.py new file mode 100644 index 0000000..91fea8b --- /dev/null +++ b/backend/backend/routes/exam.py @@ -0,0 +1,931 @@ +from flask import Blueprint, request, jsonify, session +import uuid +import random +import string +from datetime import datetime, timedelta +from pymongo import MongoClient +import os + +bp = Blueprint('exam', __name__) + +# MongoDB connection +mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') +client = MongoClient(mongo_uri) +db = client.openlearnx + +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""" + # Handle OPTIONS request for CORS + 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), + "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}") + + 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 + +@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 + +# ✅ CORRECTED: Host panel management endpoints (using Blueprint decorators) +@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: + exam = db.exams.find_one({"exam_code": exam_code.upper()}) + if not exam: + return jsonify({"success": False, "error": "Exam not found"}), 404 + + exam_info = { + "title": exam["title"], + "status": exam["status"], + "duration_minutes": exam["duration_minutes"], + "participants_count": len(exam.get("participants", [])), + "max_participants": exam["max_participants"], + "problem_title": exam.get("problem", {}).get("title", exam["title"]), + "languages": exam.get("problem", {}).get("languages", ["python"]), + "created_at": exam["created_at"], + "host_name": exam["host_name"] + } + + print(f"📊 Host panel requested info for exam {exam_code}") + return jsonify({"success": True, "exam_info": exam_info}) + except Exception as e: + print(f"❌ Error getting exam info: {str(e)}") + 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("/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/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/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/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/test", + "/api/exam/debug-join-data" + ] + }) +@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('/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', 'function_name', 'test_cases'] + 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['function_name'], + "starter_code": question_data.get('starter_code', { + 'python': f'def {question_data["function_name"]}():\n # Write your solution here\n pass' + }), + "test_cases": question_data['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['host_name'] + } + + # 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('/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 + import uuid + 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') + } + + # 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 diff --git a/backend/backend/routes/quizzes.py b/backend/backend/routes/quizzes.py new file mode 100644 index 0000000..1ca5289 --- /dev/null +++ b/backend/backend/routes/quizzes.py @@ -0,0 +1,32 @@ +from flask import Blueprint, jsonify + +bp = Blueprint('quizzes', __name__) + +# Handle both with and without trailing slash +@bp.route("/", methods=["GET"]) +@bp.route("", methods=["GET"]) # Add this line +def list_quizzes(): + quizzes = [ + { + "id": "python-quiz", + "title": "Python Fundamentals Quiz", + "topic": "Programming", + "difficulty": "Easy", + "recent_performance": 85 + }, + { + "id": "java-quiz", + "title": "Java OOP Concepts Quiz", + "topic": "Programming", + "difficulty": "Medium", + "recent_performance": 78 + }, + { + "id": "security-quiz", + "title": "Cybersecurity Basics Quiz", + "topic": "Security", + "difficulty": "Hard", + "recent_performance": 72 + } + ] + return jsonify(quizzes) diff --git a/backend/backend/routes/test_flow.py b/backend/backend/routes/test_flow.py new file mode 100644 index 0000000..d12fd6e --- /dev/null +++ b/backend/backend/routes/test_flow.py @@ -0,0 +1,108 @@ +from flask import Blueprint, request, jsonify, current_app +import jwt +from datetime import datetime + +bp = Blueprint('test', __name__) + +def get_user_from_token(token): + """Extract user from JWT token""" + try: + payload = jwt.decode( + token, + current_app.config['SECRET_KEY'], + algorithms=['HS256'] + ) + return payload['user_id'] + except: + return None + +@bp.route('/start', methods=['POST']) +async def start_test(): + """Start a new test session""" + token = request.headers.get('Authorization', '').replace('Bearer ', '') + user_id = get_user_from_token(token) + + if not user_id: + return jsonify({"error": "Authentication required"}), 401 + + data = request.get_json() + subject = data.get('subject', 'General') + + mongo_service = current_app.config['MONGO_SERVICE'] + + # Create test session + session = await mongo_service.create_test_session(user_id, subject) + + # Get first question + questions = await mongo_service.get_questions_by_difficulty(2, 1) + + if not questions: + return jsonify({"error": "No questions available"}), 404 + + question = questions[0] + session['questions'].append(str(question['_id'])) + await mongo_service.update_test_session(str(session['_id']), { + 'questions': session['questions'], + 'current_question': 0 + }) + + return jsonify({ + "session_id": str(session['_id']), + "question": { + "id": str(question['_id']), + "question": question['question'], + "options": question['options'], + "subject": question['subject'], + "difficulty": question['difficulty'] + }, + "question_number": 1, + "total_questions": 10 + }) + +@bp.route('/answer', methods=['POST']) +async def submit_answer(): + """Submit answer and get feedback""" + token = request.headers.get('Authorization', '').replace('Bearer ', '') + user_id = get_user_from_token(token) + + if not user_id: + return jsonify({"error": "Authentication required"}), 401 + + data = request.get_json() + session_id = data.get('session_id') + question_id = data.get('question_id') + answer = data.get('answer') + + mongo_service = current_app.config['MONGO_SERVICE'] + + # Get session and question + session = await mongo_service.get_test_session(session_id) + question = await mongo_service.questions.find_one({"_id": question_id}) + + if not session or not question: + return jsonify({"error": "Invalid session or question"}), 404 + + # Check answer + is_correct = answer == question['correct_answer'] + + # Provide feedback + feedback = { + "correct": is_correct, + "confidence_score": 0.85 if is_correct else 0.25, + "explanation": question['explanation'], + "correct_answer": question['options'][question['correct_answer']], + "current_score": 75.0, + "total_answered": len(session.get('answers', [])) + 1 + } + + return jsonify({ + "feedback": feedback, + "test_completed": False, + "next_question": { + "id": str(question['_id']), + "question": "Sample next question?", + "options": ["A", "B", "C", "D"], + "subject": subject, + "difficulty": 2 + } + }) diff --git a/backend/backend/seed_courses.py b/backend/backend/seed_courses.py new file mode 100644 index 0000000..7a88465 --- /dev/null +++ b/backend/backend/seed_courses.py @@ -0,0 +1,111 @@ +import asyncio +from mongo_service import MongoService +import os +from dotenv import load_dotenv + +load_dotenv() + +async def seed_courses(): + mongo_service = MongoService(os.getenv('MONGODB_URI')) + + courses = [ + { + "_id": "python-course", + "title": "Python Programming Mastery", + "subject": "Programming", + "description": "Learn Python from basics to advanced concepts including web development, data science, and automation.", + "difficulty": "Beginner to Advanced", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp", + "modules": [ + { + "id": "python-basics", + "title": "Python Fundamentals", + "lessons": [ + {"id": "variables", "title": "Variables and Data Types", "type": "video", "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp"}, + {"id": "functions", "title": "Functions and Modules", "type": "code"}, + {"id": "turtle-graphics", "title": "Python Turtle Graphics", "type": "video", "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp"} + ] + } + ] + }, + { + "_id": "java-course", + "title": "Java Development Bootcamp", + "subject": "Programming", + "description": "Master Java programming with object-oriented concepts, Spring framework, and enterprise development.", + "difficulty": "Intermediate", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp", + "modules": [ + { + "id": "java-oop", + "title": "Object-Oriented Programming in Java", + "lessons": [ + {"id": "classes", "title": "Classes and Objects", "type": "code"}, + {"id": "inheritance", "title": "Inheritance and Polymorphism", "type": "text"} + ] + } + ] + }, + { + "_id": "ethical-hacking-course", + "title": "Ethical Hacking & Cybersecurity", + "subject": "Cybersecurity", + "description": "Learn ethical hacking techniques, penetration testing, and cybersecurity fundamentals to protect systems.", + "difficulty": "Advanced", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS", + "modules": [ + { + "id": "recon", + "title": "Reconnaissance and Information Gathering", + "lessons": [ + {"id": "footprinting", "title": "Footprinting Techniques", "type": "video", "video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS"}, + {"id": "scanning", "title": "Network Scanning", "type": "code"}, + {"id": "enumeration", "title": "Service Enumeration", "type": "text"} + ] + } + ] + }, + { + "_id": "dark-web-hosting-course", + "title": "Learn Dark Web Hosting", + "subject": "Cybersecurity", + "description": "Understanding dark web infrastructure, Tor networks, and secure hosting practices for cybersecurity professionals.", + "difficulty": "Expert", + "mentor": "5t4l1n", + "video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U", + "modules": [ + { + "id": "tor-basics", + "title": "Tor Network Fundamentals", + "lessons": [ + {"id": "tor-intro", "title": "Introduction to Tor Network", "type": "video", "video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U"}, + {"id": "onion-services", "title": "Setting Up Onion Services", "type": "code"}, + {"id": "security-practices", "title": "Security Best Practices", "type": "text"} + ] + }, + { + "id": "hosting-setup", + "title": "Dark Web Hosting Setup", + "lessons": [ + {"id": "server-config", "title": "Server Configuration", "type": "code"}, + {"id": "anonymity", "title": "Maintaining Anonymity", "type": "text"} + ] + } + ] + } + ] + + try: + # Clear existing courses first + await mongo_service.db.courses.delete_many({}) + # Insert updated courses + await mongo_service.db.courses.insert_many(courses) + print("✅ Courses with mentor and video links seeded successfully!") + except Exception as e: + print(f"❌ Error seeding courses: {e}") + +if __name__ == "__main__": + asyncio.run(seed_courses()) diff --git a/backend/backend/services/compiler_service.py b/backend/backend/services/compiler_service.py new file mode 100644 index 0000000..6a77221 --- /dev/null +++ b/backend/backend/services/compiler_service.py @@ -0,0 +1,42 @@ +import docker +import tempfile +import os # ✅ Make sure this is imported +import subprocess +import time +from typing import Dict, List, Any +import json + +class CompilerService: + def __init__(self): + self.client = docker.from_env() + self.language_configs = { + 'python': { + 'image': 'python:3.9-alpine', + 'file_ext': '.py', + 'run_command': 'python /app/solution{ext}', + 'timeout': 10 + }, + 'java': { + 'image': 'openjdk:11-alpine', + 'file_ext': '.java', + 'run_command': 'cd /app && javac Solution.java && java Solution', + 'timeout': 15 + }, + 'c': { + 'image': 'gcc:9-alpine', + 'file_ext': '.c', + 'run_command': 'cd /app && gcc -o solution solution.c && ./solution', + 'timeout': 15 + }, + 'bash': { + 'image': 'bash:5-alpine', + 'file_ext': '.sh', + 'run_command': 'bash /app/solution.sh', + 'timeout': 10 + } + } + + # ... rest of your compiler service code + +# Global compiler service instance +compiler_service = CompilerService() diff --git a/backend/backend/services/real_compiler_service.py b/backend/backend/services/real_compiler_service.py new file mode 100644 index 0000000..c73070d --- /dev/null +++ b/backend/backend/services/real_compiler_service.py @@ -0,0 +1,305 @@ +import docker +import tempfile +import os +import subprocess +import time +import uuid +import json +import threading +from typing import Dict, List, Any, Optional +from datetime import datetime +import queue +import signal + +class RealCompilerService: + def __init__(self): + self.client = docker.from_env() + self.execution_queue = queue.Queue() + self.active_executions = {} + self.max_concurrent_executions = 5 + + # Enhanced language configurations with real execution + self.language_configs = { + 'python': { + 'image': 'python:3.11-slim', + 'file_ext': '.py', + 'compile_command': None, # Python doesn't need compilation + 'run_command': 'python /app/code.py', + 'timeout': 30, + 'memory_limit': '256m', + 'cpu_limit': '0.5' + }, + 'java': { + 'image': 'openjdk:17-alpine', + 'file_ext': '.java', + 'compile_command': 'javac /app/Main.java', + 'run_command': 'java -cp /app Main', + 'timeout': 30, + 'memory_limit': '512m', + 'cpu_limit': '0.5' + }, + 'cpp': { + 'image': 'gcc:latest', + 'file_ext': '.cpp', + 'compile_command': 'g++ -o /app/program /app/code.cpp -std=c++17', + 'run_command': '/app/program', + 'timeout': 30, + 'memory_limit': '256m', + 'cpu_limit': '0.5' + }, + 'c': { + 'image': 'gcc:latest', + 'file_ext': '.c', + 'compile_command': 'gcc -o /app/program /app/code.c', + 'run_command': '/app/program', + 'timeout': 30, + 'memory_limit': '256m', + 'cpu_limit': '0.5' + }, + 'javascript': { + 'image': 'node:18-alpine', + 'file_ext': '.js', + 'compile_command': None, + 'run_command': 'node /app/code.js', + 'timeout': 30, + 'memory_limit': '256m', + 'cpu_limit': '0.5' + }, + 'bash': { + 'image': 'bash:5.2-alpine3.18', + 'file_ext': '.sh', + 'compile_command': None, + 'run_command': 'bash /app/code.sh', + 'timeout': 30, + 'memory_limit': '128m', + 'cpu_limit': '0.3' + }, + 'go': { + 'image': 'golang:1.21-alpine', + 'file_ext': '.go', + 'compile_command': 'go build -o /app/program /app/code.go', + 'run_command': '/app/program', + 'timeout': 30, + 'memory_limit': '512m', + 'cpu_limit': '0.5' + }, + 'rust': { + 'image': 'rust:1.75-alpine', + 'file_ext': '.rs', + 'compile_command': 'rustc /app/code.rs -o /app/program', + 'run_command': '/app/program', + 'timeout': 60, # Rust compilation can be slow + 'memory_limit': '1g', + 'cpu_limit': '1.0' + } + } + + # Start execution worker + self.start_execution_worker() + + def start_execution_worker(self): + """Start background worker for code execution""" + def worker(): + while True: + try: + execution_task = self.execution_queue.get(timeout=1) + self._execute_task(execution_task) + self.execution_queue.task_done() + except queue.Empty: + continue + except Exception as e: + print(f"Execution worker error: {e}") + + worker_thread = threading.Thread(target=worker, daemon=True) + worker_thread.start() + + def execute_code(self, code: str, language: str, input_data: str = "", + execution_id: str = None) -> Dict[str, Any]: + """Execute code with real output capture""" + if language not in self.language_configs: + return {"error": f"Language '{language}' not supported"} + + if not execution_id: + execution_id = str(uuid.uuid4()) + + config = self.language_configs[language] + + try: + # Create execution context + execution_context = { + 'execution_id': execution_id, + 'code': code, + 'language': language, + 'input_data': input_data, + 'config': config, + 'start_time': datetime.now(), + 'status': 'running' + } + + self.active_executions[execution_id] = execution_context + + # Execute in Docker container + result = self._execute_in_container(execution_context) + + # Update execution context + execution_context['status'] = 'completed' + execution_context['end_time'] = datetime.now() + execution_context['result'] = result + + return { + "success": True, + "execution_id": execution_id, + "output": result.get('output', ''), + "error": result.get('error', ''), + "execution_time": result.get('execution_time', 0), + "memory_used": result.get('memory_used', 0), + "exit_code": result.get('exit_code', 0), + "language": language, + "timestamp": datetime.now().isoformat() + } + + except Exception as e: + return { + "error": f"Execution failed: {str(e)}", + "execution_id": execution_id, + "language": language + } + finally: + # Clean up + if execution_id in self.active_executions: + del self.active_executions[execution_id] + + def _execute_in_container(self, context: Dict) -> Dict[str, Any]: + """Execute code in secure Docker container""" + code = context['code'] + language = context['language'] + input_data = context['input_data'] + config = context['config'] + + with tempfile.TemporaryDirectory() as temp_dir: + # Prepare code file + filename = f"code{config['file_ext']}" if language != 'java' else "Main.java" + file_path = os.path.join(temp_dir, filename) + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(code) + + # Prepare input file + input_file = os.path.join(temp_dir, 'input.txt') + with open(input_file, 'w', encoding='utf-8') as f: + f.write(input_data) + + try: + start_time = time.time() + + # Create and run container + container = self.client.containers.run( + config['image'], + command=self._build_execution_command(config, filename), + volumes={temp_dir: {'bind': '/app', 'mode': 'rw'}}, + working_dir='/app', + mem_limit=config['memory_limit'], + cpu_period=100000, + cpu_quota=int(float(config['cpu_limit']) * 100000), + network_mode='none', # No network access + remove=True, + detach=False, + stdin_open=True, + tty=False, + timeout=config['timeout'], + # Security options + cap_drop=['ALL'], + security_opt=['no-new-privileges'], + read_only=False, + tmpfs={'/tmp': 'rw,noexec,nosuid,size=100m'} + ) + + execution_time = time.time() - start_time + output = container.decode('utf-8') + + return { + "output": output.strip(), + "error": "", + "exit_code": 0, + "execution_time": round(execution_time, 3), + "memory_used": self._get_memory_usage(container) + } + + except docker.errors.ContainerError as e: + return { + "output": "", + "error": f"Runtime error (exit code {e.exit_status}): {e.stderr.decode('utf-8') if e.stderr else 'Unknown error'}", + "exit_code": e.exit_status, + "execution_time": time.time() - start_time, + "memory_used": 0 + } + except docker.errors.APIError as e: + return { + "output": "", + "error": f"Docker API error: {str(e)}", + "exit_code": -1, + "execution_time": 0, + "memory_used": 0 + } + except Exception as e: + return { + "output": "", + "error": f"Execution error: {str(e)}", + "exit_code": -1, + "execution_time": 0, + "memory_used": 0 + } + + def _build_execution_command(self, config: Dict, filename: str) -> str: + """Build the execution command for the container""" + commands = [] + + # Add compilation step if needed + if config.get('compile_command'): + commands.append(config['compile_command']) + + # Add execution command with input redirection + run_cmd = config['run_command'] + if '<' not in run_cmd: # Add input redirection if not present + run_cmd += ' < /app/input.txt 2>&1' + commands.append(run_cmd) + + # Combine commands + return f"sh -c '{' && '.join(commands)}'" + + def _get_memory_usage(self, container) -> int: + """Get memory usage from container stats""" + try: + stats = container.stats(stream=False) + memory_usage = stats['memory']['usage'] + return memory_usage + except: + return 0 + + def get_supported_languages(self) -> List[Dict[str, str]]: + """Get list of supported languages with details""" + return [ + { + 'id': lang_id, + 'name': lang_id.title(), + 'extension': config['file_ext'], + 'timeout': config['timeout'], + 'memory_limit': config['memory_limit'] + } + for lang_id, config in self.language_configs.items() + ] + + def get_execution_status(self, execution_id: str) -> Optional[Dict]: + """Get status of a running execution""" + return self.active_executions.get(execution_id) + + def cancel_execution(self, execution_id: str) -> bool: + """Cancel a running execution""" + if execution_id in self.active_executions: + # Implementation would involve stopping the Docker container + del self.active_executions[execution_id] + return True + return False + +# Create global instance +real_compiler_service = RealCompilerService() diff --git a/backend/backend/services/wallet_service.py b/backend/backend/services/wallet_service.py new file mode 100644 index 0000000..bb2ad47 --- /dev/null +++ b/backend/backend/services/wallet_service.py @@ -0,0 +1,53 @@ +from web3 import Web3 +from eth_account import Account +from datetime import datetime +import hashlib +import secrets +import os # ✅ Add this missing import + +class WalletService: + def __init__(self, web3_provider_url): + self.w3 = Web3(Web3.HTTPProvider(web3_provider_url)) + + def verify_wallet_signature(self, wallet_address, signature, message): + """Verify wallet signature for authentication""" + try: + # Recover the address from signature + message_hash = Web3.keccak(text=message) + recovered_address = self.w3.eth.account.recover_message_hash(message_hash, signature=signature) + + return recovered_address.lower() == wallet_address.lower() + except Exception as e: + print(f"Signature verification error: {e}") + return False + + def generate_auth_message(self, wallet_address, exam_code): + """Generate message for wallet signing""" + timestamp = int(datetime.now().timestamp()) + nonce = secrets.token_hex(16) + + message = f"""OpenLearnX Exam Authentication + +Wallet: {wallet_address} +Exam Code: {exam_code} +Timestamp: {timestamp} +Nonce: {nonce} + +Sign this message to join the coding exam.""" + + return message, timestamp, nonce + + def create_wallet_session(self, wallet_address, exam_code, signature): + """Create authenticated wallet session""" + session_id = hashlib.sha256(f"{wallet_address}{exam_code}{datetime.now().timestamp()}".encode()).hexdigest() + + return { + "session_id": session_id, + "wallet_address": wallet_address, + "exam_code": exam_code, + "authenticated_at": datetime.now(), + "signature": signature + } + +# Create the service instance +wallet_service = WalletService(os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545')) diff --git a/backend/backend/utils/adaptive_engine.py b/backend/backend/utils/adaptive_engine.py new file mode 100644 index 0000000..b84e680 --- /dev/null +++ b/backend/backend/utils/adaptive_engine.py @@ -0,0 +1,8 @@ +def choose_next_question(current_difficulty: int, last_answer_correct: bool) -> int: + """ + Simplified adaptive engine logic adjusting difficulty for next question. + """ + if last_answer_correct: + return min(current_difficulty + 1, 3) # max difficulty = 3 + else: + return max(current_difficulty - 1, 1) # min difficulty = 1 diff --git a/backend/backend/web3_service.py b/backend/backend/web3_service.py new file mode 100644 index 0000000..f3a5e3b --- /dev/null +++ b/backend/backend/web3_service.py @@ -0,0 +1,283 @@ +from web3 import Web3 +from eth_account.messages import encode_defunct +import json +import secrets +import time +from typing import Optional, Dict, Any +from pathlib import Path + +class Web3Service: + def __init__(self, provider_url: str, contract_address: Optional[str] = None): + self.w3 = Web3(Web3.HTTPProvider(provider_url)) + self.contract_address = contract_address + self.contract = None + + if contract_address: + self.load_contract() + + def load_contract(self): + """Load the smart contract ABI and create contract instance""" + try: + # Updated path to match Foundry's output structure + contract_path = Path('out/CertificateNFT.sol/CertificateNFT.json') + + if not contract_path.exists(): + print(f"Contract JSON not found at {contract_path}") + return + + with open(contract_path, 'r') as f: + contract_data = json.load(f) + abi = contract_data['abi'] + + self.contract = self.w3.eth.contract( + address=self.contract_address, + abi=abi + ) + print(f"Contract loaded successfully at {self.contract_address}") + + except Exception as e: + print(f"Failed to load contract: {e}") + + def generate_nonce(self) -> str: + """Generate a random nonce for signature verification""" + return secrets.token_hex(16) + + def verify_signature(self, address: str, message: str, signature: str) -> bool: + """Verify MetaMask signature""" + try: + # Create the message that was signed + message_hash = encode_defunct(text=message) + + # Recover the address from signature + recovered_address = self.w3.eth.account.recover_message( + message_hash, + signature=signature + ) + + # Compare addresses (case insensitive) + return recovered_address.lower() == address.lower() + except Exception as e: + print(f"Signature verification failed: {e}") + return False + + def mint_certificate(self, to_address: str, token_uri: str, private_key: str) -> Optional[str]: + """Mint an NFT certificate using the simple mintCertificate function""" + if not self.contract: + raise Exception("Contract not loaded") + + try: + # Get account from private key + account = self.w3.eth.account.from_key(private_key) + + # Build transaction + transaction = self.contract.functions.mintCertificate( + to_address, + token_uri + ).build_transaction({ + 'from': account.address, + 'nonce': self.w3.eth.get_transaction_count(account.address), + 'gas': 500000, # Increased gas limit + 'gasPrice': self.w3.to_wei('20', 'gwei') + }) + + # Sign and send transaction + signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key) + tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction) + + # Wait for transaction receipt + receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120) + + if receipt.status == 1: + print(f"Certificate minted successfully. TX: {receipt.transactionHash.hex()}") + return receipt.transactionHash.hex() + else: + print(f"Transaction failed. Status: {receipt.status}") + return None + + except Exception as e: + print(f"Minting failed: {e}") + return None + + def mint_certificate_with_details(self, to_address: str, token_uri: str, + subject: str, student_name: str, score: int, + private_key: str) -> Optional[str]: + """Mint an NFT certificate with detailed information""" + if not self.contract: + raise Exception("Contract not loaded") + + try: + # Get account from private key + account = self.w3.eth.account.from_key(private_key) + + # Build transaction with detailed function + transaction = self.contract.functions.mintCertificateWithDetails( + to_address, + token_uri, + subject, + student_name, + score + ).build_transaction({ + 'from': account.address, + 'nonce': self.w3.eth.get_transaction_count(account.address), + 'gas': 600000, # Higher gas for detailed function + 'gasPrice': self.w3.to_wei('20', 'gwei') + }) + + # Sign and send transaction + signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key) + tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction) + + # Wait for transaction receipt + receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120) + + if receipt.status == 1: + print(f"Detailed certificate minted successfully. TX: {receipt.transactionHash.hex()}") + return receipt.transactionHash.hex() + else: + print(f"Transaction failed. Status: {receipt.status}") + return None + + except Exception as e: + print(f"Detailed minting failed: {e}") + return None + + def get_certificate_details(self, token_id: int) -> Optional[Dict]: + """Get certificate details by token ID""" + if not self.contract: + return None + + try: + # Get certificate struct data + certificate = self.contract.functions.getCertificate(token_id).call() + + # Get owner and token URI + owner = self.contract.functions.ownerOf(token_id).call() + token_uri = self.contract.functions.tokenURI(token_id).call() + + return { + 'token_id': token_id, + 'owner': owner, + 'token_uri': token_uri, + 'subject': certificate[0], + 'student_name': certificate[1], + 'score': certificate[2], + 'timestamp': certificate[3], + 'verified': certificate[4] + } + except Exception as e: + print(f"Failed to get certificate details: {e}") + return None + + def get_user_certificates(self, user_address: str) -> Optional[list]: + """Get all certificate token IDs for a user""" + if not self.contract: + return None + + try: + token_ids = self.contract.functions.getUserCertificates(user_address).call() + return token_ids + except Exception as e: + print(f"Failed to get user certificates: {e}") + return None + + def verify_certificate(self, token_id: int) -> bool: + """Verify if a certificate is valid""" + if not self.contract: + return False + + try: + is_verified = self.contract.functions.verifyCertificate(token_id).call() + return is_verified + except Exception as e: + print(f"Failed to verify certificate: {e}") + return False + + def get_total_supply(self) -> int: + """Get total number of certificates minted""" + if not self.contract: + return 0 + + try: + total = self.contract.functions.totalSupply().call() + return total + except Exception as e: + print(f"Failed to get total supply: {e}") + return 0 + + def get_latest_token_id(self) -> int: + """Get the latest token ID (useful for getting newly minted certificate)""" + return self.get_total_supply() + + def get_transaction_receipt(self, tx_hash: str) -> Optional[Dict]: + """Get transaction receipt for a given hash""" + try: + receipt = self.w3.eth.get_transaction_receipt(tx_hash) + return { + 'transaction_hash': receipt.transactionHash.hex(), + 'block_number': receipt.blockNumber, + 'gas_used': receipt.gasUsed, + 'status': receipt.status, + 'contract_address': receipt.contractAddress + } + except Exception as e: + print(f"Failed to get transaction receipt: {e}") + return None + + def is_connected(self) -> bool: + """Check if connected to blockchain""" + try: + return self.w3.is_connected() + except: + return False + + def get_balance(self, address: str) -> float: + """Get ETH balance for an address""" + try: + balance_wei = self.w3.eth.get_balance(address) + return self.w3.from_wei(balance_wei, 'ether') + except Exception as e: + print(f"Failed to get balance: {e}") + return 0.0 + + def get_gas_price(self) -> int: + """Get current gas price""" + try: + return self.w3.eth.gas_price + except Exception as e: + print(f"Failed to get gas price: {e}") + return self.w3.to_wei('20', 'gwei') # Default fallback + + def estimate_gas(self, to_address: str, token_uri: str, account_address: str) -> int: + """Estimate gas for certificate minting""" + if not self.contract: + return 500000 # Default estimate + + try: + gas_estimate = self.contract.functions.mintCertificate( + to_address, + token_uri + ).estimate_gas({'from': account_address}) + + # Add 20% buffer + return int(gas_estimate * 1.2) + + except Exception as e: + print(f"Failed to estimate gas: {e}") + return 500000 # Default fallback + + def get_contract_info(self) -> Dict: + """Get basic contract information""" + if not self.contract: + return {} + + try: + return { + 'address': self.contract_address, + 'total_certificates': self.get_total_supply(), + 'is_connected': self.is_connected(), + 'network_id': self.w3.eth.chain_id, + 'latest_block': self.w3.eth.block_number + } + except Exception as e: + print(f"Failed to get contract info: {e}") + return {} diff --git a/backend/main.py b/backend/main.py index 441456a..3655a54 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,6 +7,7 @@ from flask import Flask, jsonify, request from flask_cors import CORS from dotenv import load_dotenv from pymongo import MongoClient +from bson import ObjectId # Load env vars load_dotenv() @@ -51,14 +52,9 @@ app.config.update( ADMIN_TOKEN=os.getenv('ADMIN_TOKEN', '') ) -# CORS - Allow all endpoints under /api/* +# CORS CORS(app, resources={r"/api/*": { - "origins": [ - "http://localhost:3000", - "http://127.0.0.1:3000", - "http://localhost:3001", - "http://127.0.0.1:3001" - ], + "origins": ["http://localhost:3000", "http://127.0.0.1:3000"], "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization", "Accept", "Origin", "X-Requested-With"], "supports_credentials": True, @@ -68,13 +64,10 @@ CORS(app, resources={r"/api/*": { # Logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", - handlers=[ - logging.StreamHandler(), - logging.FileHandler("openlearnx.log") if os.access('.', os.W_OK) else logging.NullHandler() - ]) + handlers=[logging.StreamHandler()]) logger = logging.getLogger(__name__) -# Initialize MongoDB service +# Initialize services try: mongo_service = MongoService(app.config['MONGODB_URI']) app.config['MONGO_SERVICE'] = mongo_service @@ -84,7 +77,6 @@ except Exception as e: MONGO_SERVICE_AVAILABLE = False logger.error(f"❌ Failed MongoService init: {e}") -# Initialize Web3 service try: web3_service = Web3Service(app.config['WEB3_PROVIDER_URL'], app.config['CONTRACT_ADDRESS']) app.config['WEB3_SERVICE'] = web3_service @@ -130,24 +122,23 @@ for bp, prefix in [ blueprints_failed.append((prefix, str(e))) logger.error(f"❌ Blueprint {prefix} failed: {e}") -# ✅ Direct MongoDB connection for direct routes +# Database connection def get_db(): """Get MongoDB database connection""" client = MongoClient(app.config['MONGODB_URI']) return client.openlearnx # =================================================================== -# ✅ FIXED DYNAMIC SCORING SYSTEM +# ✅ DYNAMIC SCORING SYSTEM # =================================================================== def calculate_dynamic_score(code, language, problem): - """Calculate score based on test cases and expected outputs - FIXED VERSION""" + """Calculate score based on test cases and expected outputs""" import io from contextlib import redirect_stdout, redirect_stderr import time test_cases = problem.get('test_cases', []) - scoring_method = problem.get('scoring_method', 'test_cases') total_points = problem.get('total_points', 100) start_time = time.time() @@ -156,11 +147,10 @@ def calculate_dynamic_score(code, language, problem): test_results = [] points_earned = 0 - print(f"🧮 Starting FIXED dynamic scoring - {total_tests} test cases") + print(f"🧮 Dynamic scoring - {total_tests} test cases, {total_points} total points") try: if test_cases: - # ✅ FIXED TEST CASE BASED SCORING for i, test_case in enumerate(test_cases): test_input = test_case.get('input', '') expected_output = test_case.get('expected_output', '').strip() @@ -169,28 +159,20 @@ def calculate_dynamic_score(code, language, problem): print(f"📋 Test {i+1}: Input='{test_input}', Expected='{expected_output}', Points={test_points}") try: - # ✅ FIXED: Better code execution with input handling stdout_buffer = io.StringIO() stderr_buffer = io.StringIO() - # Create execution environment exec_globals = {"__builtins__": __builtins__} - - # Handle input if provided if test_input: - # Mock input function for test cases with input exec_globals['input'] = lambda prompt='': test_input - # Execute the code with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): exec(code, exec_globals) actual_output = stdout_buffer.getvalue().strip() - stderr_content = stderr_buffer.getvalue() print(f"🔍 Test {i+1} - Actual: '{actual_output}', Expected: '{expected_output}'") - # ✅ FIXED: Exact string comparison if actual_output == expected_output: passed_tests += 1 points_earned += test_points @@ -229,9 +211,8 @@ def calculate_dynamic_score(code, language, problem): "error": str(e), "description": test_case.get('description', f'Test case {i+1}') }) - else: - # ✅ FALLBACK: BASIC EXECUTION TEST + # Fallback: Basic execution test try: stdout_buffer = io.StringIO() stderr_buffer = io.StringIO() @@ -276,11 +257,9 @@ def calculate_dynamic_score(code, language, problem): }] execution_time = time.time() - start_time - - # Calculate final score percentage final_score = int((points_earned / total_points) * 100) if total_points > 0 else 0 - print(f"🏆 FIXED FINAL SCORE: {final_score}% ({points_earned}/{total_points} points, {passed_tests}/{total_tests} tests)") + print(f"🏆 FINAL SCORE: {final_score}% ({points_earned}/{total_points} points, {passed_tests}/{total_tests} tests)") return { 'score': final_score, @@ -291,17 +270,17 @@ def calculate_dynamic_score(code, language, problem): 'details': { 'points_earned': points_earned, 'total_points': total_points, - 'scoring_method': scoring_method + 'scoring_method': 'test_cases' } } # =================================================================== -# ✅ FIXED SUBMIT SOLUTION ENDPOINT +# ✅ SUBMIT SOLUTION WITH GUARANTEED LEADERBOARD UPDATE # =================================================================== @app.route('/api/exam/submit-solution', methods=['POST', 'OPTIONS']) def submit_solution_direct(): - """FIXED solution submission with proper leaderboard update""" + """Submit solution with guaranteed leaderboard update""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") @@ -314,14 +293,13 @@ def submit_solution_direct(): exam_code = data.get('exam_code', '').upper() language = data.get('language', 'python') code = data.get('code', '').strip() - participant_name = data.get('participant_name', 'Anonymous') + participant_name = data.get('participant_name', 'Anonymous').strip() - print(f"📤 FIXED SUBMIT: Exam {exam_code}, Language: {language}, Participant: {participant_name}") + print(f"📤 SUBMIT: Exam {exam_code}, Participant: '{participant_name}'") if not exam_code or not code or not participant_name: return jsonify({"success": False, "error": "Missing required data"}), 400 - # Get database db = get_db() # Find the exam @@ -337,14 +315,15 @@ def submit_solution_direct(): if not problem: return jsonify({"success": False, "error": "No problem found for this exam"}), 400 - # Check if participant already submitted + # Check for existing submission existing_submission = db.submissions.find_one({ "exam_code": exam_code, "participant_name": participant_name }) if existing_submission: - return jsonify({"success": False, "error": "You have already submitted a solution"}), 400 + print(f"⚠️ Participant {participant_name} already has submission") + return jsonify({"success": False, "error": f"Participant '{participant_name}' has already submitted"}), 400 # Calculate score scoring_result = calculate_dynamic_score(code, language, problem) @@ -366,50 +345,49 @@ def submit_solution_direct(): } # Save submission to database - db.submissions.insert_one(submission) - print(f"💾 Submission saved to database with ID: {submission['submission_id']}") + submission_result = db.submissions.insert_one(submission) + print(f"💾 Submission saved with ID: {submission_result.inserted_id}") - # ✅ FIXED: Ensure participant exists in participants collection first - participant_data = { + # Delete old participant record and create new completed one + print(f"🗑️ Deleting any existing participant records for {participant_name}") + delete_result = db.participants.delete_many({"exam_code": exam_code, "name": participant_name}) + print(f"🗑️ Deleted {delete_result.deleted_count} old participant records") + + # Create fresh completed participant record + participant_record = { "exam_code": exam_code, "name": participant_name, - "joined_at": datetime.now(), - "completed": True, + "completed": True, # CRITICAL: Must be True "score": scoring_result['score'], "submitted_at": datetime.now(), + "joined_at": datetime.now(), "language": language, "passed_tests": scoring_result['passed_tests'], "total_tests": scoring_result['total_tests'], "points_earned": scoring_result['details']['points_earned'], - "total_points": scoring_result['details']['total_points'] + "total_points": scoring_result['details']['total_points'], + "session_id": str(uuid.uuid4()), + "rank": 0, + "updated_at": datetime.now() } - # ✅ FIXED: Use replace_one to ensure participant record exists - participant_result = db.participants.replace_one( - {"exam_code": exam_code, "name": participant_name}, - participant_data, - upsert=True - ) + # Insert fresh participant record + participant_result = db.participants.insert_one(participant_record) + print(f"👤 NEW participant record created with ID: {participant_result.inserted_id}") - if participant_result.upserted_id: - print(f"👤 NEW participant record created for {participant_name}") - elif participant_result.modified_count > 0: - print(f"👤 UPDATED existing participant record for {participant_name}") - else: - print(f"⚠️ No changes to participant record for {participant_name}") - - # ✅ VERIFY: Check if participant was actually saved + # Verification verification = db.participants.find_one({"exam_code": exam_code, "name": participant_name}) - if verification: - print(f"✅ VERIFIED: Participant {participant_name} exists in leaderboard with score {verification.get('score', 'unknown')}") + if verification and verification.get('completed'): + print(f"✅ VERIFICATION SUCCESS: {participant_name} completed={verification.get('completed')}, score={verification.get('score')}") else: - print(f"❌ ERROR: Participant {participant_name} NOT found in participants collection!") + print(f"❌ VERIFICATION FAILED: Participant record not found or not completed") + return jsonify({"success": False, "error": "Failed to update participant status"}), 500 - print(f"✅ FIXED SUBMISSION COMPLETE - Score: {scoring_result['score']}% ({scoring_result['passed_tests']}/{scoring_result['total_tests']} tests passed)") + print(f"✅ SUBMIT COMPLETE - {participant_name}: {scoring_result['score']}% ({scoring_result['passed_tests']}/{scoring_result['total_tests']} tests)") return jsonify({ "success": True, - "message": "Solution submitted successfully with fixed leaderboard update!", + "message": f"Solution submitted successfully for {participant_name}!", "score": scoring_result['score'], "passed_tests": scoring_result['passed_tests'], "total_tests": scoring_result['total_tests'], @@ -422,67 +400,18 @@ def submit_solution_direct(): }) except Exception as e: - print(f"❌ Fixed submit error: {str(e)}") + print(f"❌ Submit error: {str(e)}") import traceback traceback.print_exc() - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"success": False, "error": f"Submission failed: {str(e)}"}), 500 + +# =================================================================== +# ✅ ULTIMATE LEADERBOARD FIX - Handles duplicates and forces sync +# =================================================================== -@app.route('/api/debug/leaderboard-check//', methods=['GET']) -def debug_leaderboard_check(exam_code, participant_name): - """Debug specific participant in leaderboard""" - try: - db = get_db() - - # Check submissions - submissions = list(db.submissions.find({ - "exam_code": exam_code.upper(), - "participant_name": participant_name - })) - - # Check participants - participants = list(db.participants.find({ - "exam_code": exam_code.upper(), - "name": participant_name - })) - - # Check all participants for this exam - all_participants = list(db.participants.find({"exam_code": exam_code.upper()})) - - # Convert ObjectIds - for sub in submissions: - if '_id' in sub: - sub['_id'] = str(sub['_id']) - - for part in participants: - if '_id' in part: - part['_id'] = str(part['_id']) - - for part in all_participants: - if '_id' in part: - part['_id'] = str(part['_id']) - - return jsonify({ - "success": True, - "exam_code": exam_code.upper(), - "participant_name": participant_name, - "submissions_found": len(submissions), - "submissions": submissions, - "participants_found": len(participants), - "participants": participants, - "all_participants_in_exam": len(all_participants), - "all_participants": all_participants, - "debug_info": { - "should_be_in_leaderboard": len(submissions) > 0, - "is_in_participants_collection": len(participants) > 0, - "participant_names_in_exam": [p.get('name') for p in all_participants] - } - }) - - except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/exam/leaderboard/', methods=['GET', 'OPTIONS']) def get_leaderboard_direct(exam_code): - """Enhanced leaderboard with better debugging""" + """ULTIMATE LEADERBOARD FIX - Handles duplicates and forces correct sync""" if request.method == "OPTIONS": response = jsonify({'status': 'ok'}) response.headers.add("Access-Control-Allow-Origin", "*") @@ -491,54 +420,92 @@ def get_leaderboard_direct(exam_code): return response try: - print(f"🏆 Getting FIXED leaderboard for: {exam_code}") + print(f"🏆 ULTIMATE LEADERBOARD FIX: {exam_code}") db = get_db() + # Get exam info exam = db.exams.find_one({"exam_code": exam_code.upper()}) if not exam: return jsonify({"success": False, "error": "Exam not found"}), 404 - # Get ALL participants - participants = list(db.participants.find({"exam_code": exam_code.upper()})) - print(f"👥 Found {len(participants)} participants in database") + # ✅ ULTIMATE FIX: Get unique submissions (latest per participant) using aggregation + print(f"🔄 Getting latest submission per participant...") - # Debug: Print participant names - for p in participants: - print(f" - {p.get('name', 'NO_NAME')}: completed={p.get('completed', False)}, score={p.get('score', 0)}") + # Use MongoDB aggregation to get latest submission per participant + pipeline = [ + {"$match": {"exam_code": exam_code.upper()}}, + {"$sort": {"submitted_at": -1}}, # Sort by latest first + {"$group": { + "_id": "$participant_name", # Group by participant name + "latest_submission": {"$first": "$$ROOT"} # Take the first (latest) submission + }}, + {"$replaceRoot": {"newRoot": "$latest_submission"}} # Replace root with the submission + ] - # Sort completed participants by score - completed_participants = [p for p in participants if p.get('completed', False)] - completed_participants.sort(key=lambda x: x.get('score', 0), reverse=True) + unique_submissions = list(db.submissions.aggregate(pipeline)) + print(f"📋 Found {len(unique_submissions)} unique submissions after deduplication") - print(f"🏁 {len(completed_participants)} completed participants for leaderboard") + # Debug: Print unique submissions + for sub in unique_submissions: + print(f" - {sub.get('participant_name')}: {sub.get('score')}% at {sub.get('submitted_at')}") + # ✅ FORCE REBUILD: Delete ALL participants and recreate from unique submissions + delete_result = db.participants.delete_many({"exam_code": exam_code.upper()}) + print(f"🗑️ Deleted {delete_result.deleted_count} old participant records") + + # Create leaderboard from unique submissions leaderboard = [] - for i, participant in enumerate(completed_participants): - participant['rank'] = i + 1 - if '_id' in participant: - participant['_id'] = str(participant['_id']) + for submission in unique_submissions: + participant_name = submission.get('participant_name') + if not participant_name: + continue + + # Create completed participant record + participant = { + "exam_code": exam_code.upper(), + "name": participant_name, + "completed": True, # ✅ ALWAYS TRUE + "score": submission.get('score', 0), + "submitted_at": submission.get('submitted_at'), + "joined_at": submission.get('submitted_at'), + "language": submission.get('language'), + "passed_tests": submission.get('passed_tests', 0), + "total_tests": submission.get('total_tests', 1), + "points_earned": submission.get('scoring_details', {}).get('points_earned', 0), + "total_points": submission.get('scoring_details', {}).get('total_points', 100), + "session_id": f"ultimate-{uuid.uuid4()}", + "rank": 0 # Will be set below + } + + # Insert participant and add to leaderboard + result = db.participants.insert_one(participant) + participant['_id'] = str(result.inserted_id) leaderboard.append(participant) + + print(f"✅ CREATED: {participant_name} with score {submission.get('score', 0)}%") - # Get waiting participants - waiting_participants = [p for p in participants if not p.get('completed', False)] - for participant in waiting_participants: - if '_id' in participant: - participant['_id'] = str(participant['_id']) + # Sort by score (highest first) and assign ranks + leaderboard.sort(key=lambda x: x.get('score', 0), reverse=True) + for i, participant in enumerate(leaderboard): + participant['rank'] = i + 1 + # Update rank in database + db.participants.update_one( + {"_id": ObjectId(participant['_id'])}, + {"$set": {"rank": i + 1}} + ) - print(f"⏳ {len(waiting_participants)} waiting participants") - - # Calculate stats - scores = [p.get('score', 0) for p in completed_participants] - passed_tests = [p.get('passed_tests', 0) for p in completed_participants] - total_tests = [p.get('total_tests', 1) for p in completed_participants] + # Calculate accurate stats + scores = [p.get('score', 0) for p in leaderboard] + passed_tests = [p.get('passed_tests', 0) for p in leaderboard] + total_tests = [p.get('total_tests', 1) for p in leaderboard] stats = { - "total_participants": len(participants), - "completed_submissions": len(completed_participants), - "waiting_submissions": len(waiting_participants), - "average_score": sum(scores) / len(scores) if scores else 0, + "total_participants": len(leaderboard), + "completed_submissions": len(leaderboard), + "waiting_submissions": 0, # No waiting since we only show submitted participants + "average_score": round(sum(scores) / len(scores)) if scores else 0, "highest_score": max(scores) if scores else 0, - "average_tests_passed": sum(passed_tests) / len(passed_tests) if passed_tests else 0, + "average_tests_passed": round(sum(passed_tests) / len(passed_tests)) if passed_tests else 0, "total_test_cases": max(total_tests) if total_tests else 1, "blockchain_participants": 0 } @@ -546,215 +513,29 @@ def get_leaderboard_direct(exam_code): if '_id' in exam: exam['_id'] = str(exam['_id']) - print(f"📊 Leaderboard stats: {stats}") + print(f"🎯 ULTIMATE LEADERBOARD COMPLETE:") + print(f" - Unique participants: {len(leaderboard)}") + print(f" - Waiting participants: 0") + print(f" - Average score: {stats['average_score']}%") return jsonify({ "success": True, "leaderboard": leaderboard, - "waiting_participants": waiting_participants, + "waiting_participants": [], # Always empty - only show completed participants "stats": stats, - "exam_info": exam + "exam_info": exam, + "ultimate_fix_applied": True, + "unique_submissions_processed": len(unique_submissions) }) except Exception as e: - print(f"❌ Enhanced leaderboard error: {str(e)}") - return jsonify({"success": False, "error": str(e)}), 500 - -@app.route('/api/exam/submit-solution', methods=['POST', 'OPTIONS']) -def submit_solution_direct(): - """FIXED solution submission handling name mismatch""" - 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() - language = data.get('language', 'python') - code = data.get('code', '').strip() - participant_name = data.get('participant_name', 'Anonymous') - - print(f"📤 SUBMIT: Exam {exam_code}, Language: {language}, Participant: '{participant_name}'") - - if not exam_code or not code: - return jsonify({"success": False, "error": "Missing required data"}), 400 - - # Get database - db = get_db() - - # Find the exam - exam = db.exams.find_one({"exam_code": exam_code}) - if not exam: - return jsonify({"success": False, "error": "Exam not found"}), 404 - - if exam.get('status') != 'active': - return jsonify({"success": False, "error": "Exam is not active"}), 400 - - # Get the problem - problem = exam.get('problem', {}) - if not problem: - return jsonify({"success": False, "error": "No problem found for this exam"}), 400 - - # ✅ FIXED: Handle name mismatch - find existing participant - existing_participant = db.participants.find_one({"exam_code": exam_code}) - - if existing_participant and not existing_participant.get('completed', False): - # Use the existing participant's name instead of submitted name - actual_participant_name = existing_participant.get('name', participant_name) - print(f"🔄 Using existing participant name: '{actual_participant_name}' instead of '{participant_name}'") - else: - actual_participant_name = participant_name - - # Check if this participant already submitted - existing_submission = db.submissions.find_one({ - "exam_code": exam_code, - "participant_name": actual_participant_name - }) - - if existing_submission: - return jsonify({"success": False, "error": f"Participant '{actual_participant_name}' already submitted"}), 400 - - # Calculate score - scoring_result = calculate_dynamic_score(code, language, problem) - - # Store submission - submission = { - "exam_code": exam_code, - "participant_name": actual_participant_name, # ✅ Use actual name - "language": language, - "code": code, - "score": scoring_result['score'], - "passed_tests": scoring_result['passed_tests'], - "total_tests": scoring_result['total_tests'], - "test_results": scoring_result['test_results'], - "execution_time": scoring_result['execution_time'], - "submitted_at": datetime.now(), - "submission_id": str(uuid.uuid4()), - "scoring_details": scoring_result['details'] - } - - # Save submission - db.submissions.insert_one(submission) - print(f"💾 Submission saved for participant: '{actual_participant_name}'") - - # ✅ FIXED: Update the correct participant record - update_result = db.participants.update_one( - {"exam_code": exam_code, "name": actual_participant_name}, - { - "$set": { - "completed": True, - "score": scoring_result['score'], - "submitted_at": datetime.now(), - "language": language, - "passed_tests": scoring_result['passed_tests'], - "total_tests": scoring_result['total_tests'], - "points_earned": scoring_result['details']['points_earned'], - "total_points": scoring_result['details']['total_points'] - } - } - ) - - if update_result.modified_count > 0: - print(f"✅ UPDATED participant '{actual_participant_name}' - completed: true, score: {scoring_result['score']}%") - else: - print(f"❌ FAILED to update participant '{actual_participant_name}'") - # Create new participant record if update failed - participant_data = { - "exam_code": exam_code, - "name": actual_participant_name, - "joined_at": datetime.now(), - "completed": True, - "score": scoring_result['score'], - "submitted_at": datetime.now(), - "language": language, - "passed_tests": scoring_result['passed_tests'], - "total_tests": scoring_result['total_tests'], - "points_earned": scoring_result['details']['points_earned'], - "total_points": scoring_result['details']['total_points'] - } - db.participants.insert_one(participant_data) - print(f"🆕 CREATED new participant record for '{actual_participant_name}'") - - # Verify the update worked - verification = db.participants.find_one({"exam_code": exam_code, "name": actual_participant_name}) - if verification and verification.get('completed'): - print(f"✅ VERIFIED: '{actual_participant_name}' is completed with score {verification.get('score')}%") - else: - print(f"❌ VERIFICATION FAILED for '{actual_participant_name}'") - - return jsonify({ - "success": True, - "message": f"Solution submitted successfully for {actual_participant_name}!", - "score": scoring_result['score'], - "passed_tests": scoring_result['passed_tests'], - "total_tests": scoring_result['total_tests'], - "test_results": scoring_result['test_results'], - "execution_time": scoring_result['execution_time'], - "submission_id": submission["submission_id"], - "scoring_details": scoring_result['details'], - "participant_name": actual_participant_name, - "original_name": participant_name - }) - - except Exception as e: - print(f"❌ Submit error: {str(e)}") + print(f"❌ Ultimate leaderboard error: {str(e)}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 - # =================================================================== -# ✅ DEBUG ENDPOINT FOR TROUBLESHOOTING -# =================================================================== - -@app.route('/api/debug/submissions/', methods=['GET']) -def debug_submissions(exam_code): - """Debug endpoint to check submissions and participants""" - try: - db = get_db() - - # Get all submissions for this exam - submissions = list(db.submissions.find({"exam_code": exam_code.upper()})) - participants = list(db.participants.find({"exam_code": exam_code.upper()})) - exam = db.exams.find_one({"exam_code": exam_code.upper()}) - - # Convert ObjectId to string - for submission in submissions: - if '_id' in submission: - submission['_id'] = str(submission['_id']) - - for participant in participants: - if '_id' in participant: - participant['_id'] = str(participant['_id']) - - if exam and '_id' in exam: - exam['_id'] = str(exam['_id']) - - return jsonify({ - "success": True, - "exam_code": exam_code.upper(), - "exam_info": exam, - "submissions": submissions, - "participants": participants, - "submission_count": len(submissions), - "participant_count": len(participants), - "debug_info": { - "exam_found": exam is not None, - "exam_status": exam.get('status') if exam else 'not found', - "has_problem": bool(exam.get('problem')) if exam else False, - "test_cases_count": len(exam.get('problem', {}).get('test_cases', [])) if exam else 0 - } - }) - - except Exception as e: - print(f"❌ Debug error: {str(e)}") - return jsonify({"success": False, "error": str(e)}), 500 - -# =================================================================== -# ✅ ALL OTHER EXISTING ENDPOINTS (keeping your current ones) +# ✅ OTHER EXAM ENDPOINTS # =================================================================== @app.route('/api/exam/upload-question', methods=['POST', 'OPTIONS']) @@ -768,31 +549,25 @@ def upload_question_direct(): return response try: - print(f"📤 Enhanced question upload request") - data = request.get_json() exam_code = data.get('exam_code', '').upper() question_data = data.get('question', {}) - print(f"📝 Uploading question '{question_data.get('title')}' to exam {exam_code}") - if not exam_code or not question_data: return jsonify({"success": False, "error": "Missing exam_code or question data"}), 400 - # Get database db = get_db() # Find the exam exam = db.exams.find_one({"exam_code": exam_code}) if not exam: - print(f"❌ Exam {exam_code} not found") 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 questions after exam has started"}), 400 - # ✅ ENHANCED QUESTION STRUCTURE WITH DYNAMIC SCORING + # Enhanced question structure question = { "id": str(uuid.uuid4()), "title": question_data.get('title', 'Custom Question'), @@ -802,14 +577,13 @@ def upload_question_direct(): "starter_code": question_data.get('starter_code', { 'python': 'def solve():\n # Write your solution here\n pass' }), - # ✅ ENHANCED TEST CASES FOR DYNAMIC SCORING "test_cases": question_data.get('test_cases', [ { "input": "", "expected_output": "Hello World", "description": "Basic test case", "is_public": True, - "points": 25 + "points": 100 } ]), "examples": question_data.get('examples', []), @@ -819,7 +593,6 @@ def upload_question_direct(): "created_at": datetime.now(), "uploaded_by": exam.get('host_name', 'Host'), "languages": list(question_data.get('starter_code', {}).keys()), - # ✅ HOST'S CORRECT SOLUTION "correct_solution": { "python": question_data.get('correct_solution', {}).get('python', ''), "java": question_data.get('correct_solution', {}).get('java', ''), @@ -842,7 +615,6 @@ def upload_question_direct(): ) if result.modified_count > 0: - print(f"✅ Enhanced question '{question['title']}' uploaded to exam {exam_code}") return jsonify({ "success": True, "message": "Question uploaded successfully with dynamic scoring", @@ -852,55 +624,13 @@ def upload_question_direct(): "total_points": question['total_points'] }) else: - print(f"❌ Failed to update exam {exam_code}") return jsonify({"success": False, "error": "Failed to update exam"}), 500 except Exception as e: - print(f"❌ Error: {str(e)}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 -# Keep all your other existing endpoints... -@app.route('/api/exam/update-duration', methods=['POST', 'OPTIONS']) -def update_duration_direct(): - """Direct endpoint for duration update""" - 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"⏰ DIRECT: Updating duration for exam {exam_code} to {duration_minutes} minutes") - - if not exam_code or duration_minutes <= 0: - return jsonify({"success": False, "error": "Invalid data"}), 400 - - db = get_db() - result = db.exams.update_one( - {"exam_code": exam_code, "status": "waiting"}, - {"$set": {"duration_minutes": duration_minutes}} - ) - - if result.modified_count > 0: - print(f"✅ Duration updated to {duration_minutes} minutes") - return jsonify({ - "success": True, - "message": f"Duration updated to {duration_minutes} minutes" - }) - else: - return jsonify({"success": False, "error": "Exam not found or already started"}), 404 - - except Exception as e: - print(f"❌ Error: {str(e)}") - return jsonify({"success": False, "error": str(e)}), 500 - @app.route('/api/exam/info/', methods=['GET', 'OPTIONS']) def get_exam_info_direct(exam_code): """Get exam information""" @@ -912,7 +642,6 @@ def get_exam_info_direct(exam_code): return response try: - print(f"📋 Getting exam info for: {exam_code}") db = get_db() exam = db.exams.find_one({"exam_code": exam_code.upper()}) @@ -928,7 +657,6 @@ def get_exam_info_direct(exam_code): }) except Exception as e: - print(f"❌ Get exam info error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/exam/get-problem/', methods=['GET', 'OPTIONS']) @@ -942,7 +670,6 @@ def get_exam_problem_direct(exam_code): return response try: - print(f"📝 Getting problem for exam: {exam_code}") db = get_db() exam = db.exams.find_one({"exam_code": exam_code.upper()}) @@ -959,100 +686,6 @@ def get_exam_problem_direct(exam_code): }) except Exception as e: - print(f"❌ Get problem error: {str(e)}") - return jsonify({"success": False, "error": str(e)}), 500 - -@app.route('/api/exam/participants/', methods=['GET', 'OPTIONS']) -def get_participants_direct(exam_code): - """Get exam 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"👥 Getting participants for: {exam_code}") - db = get_db() - participants = list(db.participants.find({"exam_code": exam_code.upper()})) - - for participant in participants: - if '_id' in participant: - participant['_id'] = str(participant['_id']) - - return jsonify({ - "success": True, - "participants": participants - }) - - except Exception as e: - print(f"❌ Get participants error: {str(e)}") - return jsonify({"success": False, "error": str(e)}), 500 - -@app.route('/api/exam/leaderboard/', methods=['GET', 'OPTIONS']) -def get_leaderboard_direct(exam_code): - """Enhanced leaderboard with dynamic scoring details""" - 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"🏆 Getting enhanced leaderboard for: {exam_code}") - db = get_db() - - exam = db.exams.find_one({"exam_code": exam_code.upper()}) - if not exam: - return jsonify({"success": False, "error": "Exam not found"}), 404 - - participants = list(db.participants.find({"exam_code": exam_code.upper()})) - - completed_participants = [p for p in participants if p.get('completed', False)] - completed_participants.sort(key=lambda x: x.get('score', 0), reverse=True) - - leaderboard = [] - for i, participant in enumerate(completed_participants): - participant['rank'] = i + 1 - if '_id' in participant: - participant['_id'] = str(participant['_id']) - leaderboard.append(participant) - - waiting_participants = [p for p in participants if not p.get('completed', False)] - for participant in waiting_participants: - if '_id' in participant: - participant['_id'] = str(participant['_id']) - - scores = [p.get('score', 0) for p in completed_participants] - passed_tests = [p.get('passed_tests', 0) for p in completed_participants] - total_tests = [p.get('total_tests', 1) for p in completed_participants] - - stats = { - "total_participants": len(participants), - "completed_submissions": len(completed_participants), - "waiting_submissions": len(waiting_participants), - "average_score": sum(scores) / len(scores) if scores else 0, - "highest_score": max(scores) if scores else 0, - "average_tests_passed": sum(passed_tests) / len(passed_tests) if passed_tests else 0, - "total_test_cases": max(total_tests) if total_tests else 1, - "blockchain_participants": 0 - } - - if '_id' in exam: - exam['_id'] = str(exam['_id']) - - return jsonify({ - "success": True, - "leaderboard": leaderboard, - "waiting_participants": waiting_participants, - "stats": stats, - "exam_info": exam - }) - - except Exception as e: - print(f"❌ Enhanced leaderboard error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/exam/start-exam', methods=['POST', 'OPTIONS']) @@ -1069,8 +702,6 @@ def start_exam_direct(): data = request.get_json() exam_code = data.get('exam_code', '').upper() - print(f"🚀 Starting exam: {exam_code}") - db = get_db() exam = db.exams.find_one({"exam_code": exam_code}) if not exam: @@ -1096,7 +727,6 @@ def start_exam_direct(): ) if result.modified_count > 0: - print(f"✅ Exam {exam_code} started successfully") return jsonify({ "success": True, "message": "Exam started successfully", @@ -1107,7 +737,6 @@ def start_exam_direct(): return jsonify({"success": False, "error": "Failed to start exam"}), 500 except Exception as e: - print(f"❌ Start exam error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/exam/stop-exam', methods=['POST', 'OPTIONS']) @@ -1124,8 +753,6 @@ def stop_exam_direct(): data = request.get_json() exam_code = data.get('exam_code', '').upper() - print(f"🛑 Stopping exam: {exam_code}") - db = get_db() result = db.exams.update_one( {"exam_code": exam_code}, @@ -1139,7 +766,6 @@ def stop_exam_direct(): ) if result.modified_count > 0: - print(f"✅ Exam {exam_code} stopped successfully") return jsonify({ "success": True, "message": "Exam stopped successfully" @@ -1148,9 +774,12 @@ def stop_exam_direct(): return jsonify({"success": False, "error": "Exam not found"}), 404 except Exception as e: - print(f"❌ Stop exam error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 +# =================================================================== +# ✅ COMPILER ENDPOINT +# =================================================================== + @app.route('/api/compiler/execute', methods=['POST', 'OPTIONS']) def execute_code_direct(): """Direct compiler endpoint""" @@ -1166,15 +795,12 @@ def execute_code_direct(): language = data.get('language', 'python').lower() code = data.get('code', '').strip() - print(f"🔧 COMPILER: Executing {language} code: {code}") - if not code: return jsonify({"success": False, "error": "No code provided"}), 400 if language == 'python': try: import io - import sys from contextlib import redirect_stdout, redirect_stderr import time @@ -1191,8 +817,6 @@ def execute_code_direct(): stdout_content = stdout_buffer.getvalue() stderr_content = stderr_buffer.getvalue() - print(f"✅ Python exec successful. Output: '{stdout_content.strip()}'") - return jsonify({ "success": True, "output": stdout_content or "Code executed successfully (no output)", @@ -1203,7 +827,6 @@ def execute_code_direct(): except Exception as e: execution_time = time.time() - start_time - print(f"❌ Python execution error: {str(e)}") return jsonify({ "success": False, "error": f"Runtime error: {str(e)}", @@ -1211,7 +834,6 @@ def execute_code_direct(): }) except Exception as e: - print(f"❌ Python setup error: {str(e)}") return jsonify({"success": False, "error": f"Setup failed: {str(e)}"}), 500 else: return jsonify({ @@ -1220,19 +842,86 @@ def execute_code_direct(): }) except Exception as e: - print(f"❌ Compiler endpoint error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 +# =================================================================== +# ✅ DEBUG ENDPOINTS +# =================================================================== + +@app.route('/api/debug/complete-reset/', methods=['POST', 'OPTIONS']) +def complete_reset_exam(exam_code): + """COMPLETE RESET: Fix all participant-submission mismatches""" + if request.method == "OPTIONS": + response = jsonify({'status': 'ok'}) + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Methods", "POST,OPTIONS") + return response + + try: + db = get_db() + exam_code = exam_code.upper() + + print(f"🔥 COMPLETE RESET for exam: {exam_code}") + + # 1. Get all submissions + submissions = list(db.submissions.find({"exam_code": exam_code})) + print(f"📋 Found {len(submissions)} submissions") + + # 2. DELETE ALL participants + delete_result = db.participants.delete_many({"exam_code": exam_code}) + print(f"🗑️ Deleted {delete_result.deleted_count} participant records") + + # 3. Recreate ONLY completed participants from submissions + created_count = 0 + for submission in submissions: + participant_name = submission.get('participant_name') + if not participant_name: + continue + + new_participant = { + "exam_code": exam_code, + "name": participant_name, + "completed": True, # ✅ ALWAYS TRUE for submissions + "score": submission.get('score', 0), + "submitted_at": submission.get('submitted_at'), + "joined_at": submission.get('submitted_at'), + "language": submission.get('language'), + "passed_tests": submission.get('passed_tests', 0), + "total_tests": submission.get('total_tests', 1), + "points_earned": submission.get('scoring_details', {}).get('points_earned', 0), + "total_points": submission.get('scoring_details', {}).get('total_points', 100), + "session_id": f"reset-{uuid.uuid4()}", + "rank": 0 + } + + db.participants.insert_one(new_participant) + print(f"✅ Created completed participant: {participant_name} with score {submission.get('score', 0)}%") + created_count += 1 + + print(f"🎯 COMPLETE RESET FINISHED: {created_count} participants recreated") + + return jsonify({ + "success": True, + "message": f"Complete reset finished for {exam_code}", + "submissions_found": len(submissions), + "participants_deleted": delete_result.deleted_count, + "participants_created": created_count + }) + + except Exception as e: + print(f"❌ Complete reset error: {str(e)}") + return jsonify({"success": False, "error": str(e)}), 500 + +# =================================================================== +# ✅ REQUEST HANDLERS +# =================================================================== + # Request logging @app.before_request def log_request(): path = request.path if path.startswith('/api/exam'): logger.info(f"📥 Exam request: {request.method} {path}") - if request.method in ['POST', 'PUT'] and request.is_json: - print(f"📦 Request data: {request.json}") - elif path.startswith('/api/compiler'): - logger.info(f"📥 Compiler request: {request.method} {path}") # Handle CORS preflight @app.before_request @@ -1247,22 +936,12 @@ def handle_options(): }) return resp -# Security headers -@app.after_request -def secure_headers(resp): - resp.headers.update({ - 'X-Content-Type-Options':'nosniff', - 'X-Frame-Options':'DENY', - 'X-XSS-Protection':'1; mode=block' - }) - return resp - # Health endpoints @app.route('/') def health_root(): return jsonify({ "status":"OpenLearnX API running", - "version":"2.1.0 - Fixed Dynamic Scoring", + "version":"2.4.0 - ULTIMATE LEADERBOARD FIX", "timestamp": datetime.now().isoformat(), "features":{ "mongodb": MONGO_SERVICE_AVAILABLE, @@ -1271,7 +950,7 @@ def health_root(): "compiler": COMPILER_SERVICE_AVAILABLE, "docker": check_docker_availability(), "dynamic_scoring": True, - "fixed_scoring": True + "ultimate_leaderboard_fix": True } }) @@ -1284,7 +963,7 @@ def api_health(): "wallet": WALLET_SERVICE_AVAILABLE, "compiler": COMPILER_SERVICE_AVAILABLE, "docker": check_docker_availability(), - "fixed_dynamic_scoring": True + "ultimate_leaderboard_fix": True } if MONGO_SERVICE_AVAILABLE: @@ -1309,8 +988,7 @@ def not_found(e): return jsonify({ "error": "Not Found", "path": request.path, - "method": request.method, - "available_blueprints": blueprints_registered + "method": request.method }), 404 @app.errorhandler(500) @@ -1321,55 +999,9 @@ def internal_error(e): "timestamp": datetime.now().isoformat() }), 500 -# Startup initialization -def initialize_application(): - logger.info("🚀 Initializing OpenLearnX Backend with FIXED Dynamic Scoring") - - logger.info(f"📋 Blueprints registered: {len(blueprints_registered)}") - for bp in blueprints_registered: - logger.info(f" ✅ {bp}") - - if blueprints_failed: - logger.warning(f"❌ Blueprints failed: {len(blueprints_failed)}") - for bp, error in blueprints_failed: - logger.warning(f" ❌ {bp}: {error}") - - if MONGO_SERVICE_AVAILABLE: - try: - asyncio.get_event_loop().run_until_complete(mongo_service.init_db()) - logger.info("✅ MongoDB initialized") - - db = get_db() - db.command('ismaster') - logger.info("✅ Direct MongoDB connection verified") - except Exception as e: - logger.error(f"❌ MongoDB init failed: {e}") - - if WEB3_SERVICE_AVAILABLE: - if web3_service.w3.is_connected(): - logger.info("✅ Web3 connected") - else: - logger.warning("⚠️ Web3 not connected") - - logger.info("🧮 FIXED Dynamic Scoring Features:") - scoring_features = [ - "Fixed test case execution", - "Proper participant name tracking", - "Enhanced debug endpoints", - "Fixed point distribution", - "Improved error handling" - ] - for feature in scoring_features: - logger.info(f" 🎯 {feature}") - - return True - if __name__ == '__main__': - initialize_application() - - logger.info("🚀 Starting OpenLearnX Backend Server with FIXED Dynamic Scoring") - logger.info("📚 Fixed Features: Proper Scoring Updates, Input Handling, Participant Tracking") + logger.info("🚀 Starting OpenLearnX Backend with ULTIMATE LEADERBOARD FIX") + logger.info("📚 Features: Dynamic Scoring, Duplicate Handling, Force Sync") logger.info("🌐 Server starting on http://0.0.0.0:5000") - logger.info("🔧 All /api/* endpoints have CORS enabled") app.run(host='0.0.0.0', port=5000, debug=True, threaded=True, use_reloader=False) diff --git a/frontend/app/coding/exam/[examCode]/page.tsx b/frontend/app/coding/exam/[examCode]/page.tsx index 5ed73c8..7197b35 100644 --- a/frontend/app/coding/exam/[examCode]/page.tsx +++ b/frontend/app/coding/exam/[examCode]/page.tsx @@ -1,7 +1,7 @@ 'use client' import React, { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' -import { Trophy, Clock, Users, Send, RefreshCw, Play, Code, Wallet, Shield } from 'lucide-react' +import { Trophy, Clock, Users, Send, RefreshCw, Play, Code, Wallet, Shield, TestTube } from 'lucide-react' interface Participant { name: string @@ -53,9 +53,7 @@ export default function EnhancedExamInterface() { const languageIcons: {[key: string]: string} = { python: '🐍', java: '☕', - javascript: '🌐', - cpp: '⚡', - c: '🔧', + c: '⚡', bash: '💻' } @@ -72,15 +70,15 @@ export default function EnhancedExamInterface() { // Fetch problem details fetchProblem(session.exam_code) - // Start polling for updates + // More frequent polling for real-time updates const interval = setInterval(() => { fetchLeaderboard(session.exam_code) - }, 3000) + }, 2000) return () => clearInterval(interval) }, [router]) - // ✅ FIXED TIMER COUNTDOWN + // Timer countdown useEffect(() => { if (!timerInitialized || timeRemaining <= 0) return @@ -113,58 +111,74 @@ export default function EnhancedExamInterface() { } } - // ✅ FIXED TIMER CALCULATION IN FETCHLEADERBOARD + // ✅ ENHANCED: More aggressive leaderboard fetching with better debugging const fetchLeaderboard = async (examCode: string) => { try { - const response = await fetch(`http://127.0.0.1:5000/api/exam/leaderboard/${examCode}`) + console.log('🏆 Fetching leaderboard for:', examCode) + + // Add cache busting to prevent stale data + const response = await fetch(`http://127.0.0.1:5000/api/exam/leaderboard/${examCode}?t=${Date.now()}`) const data = await response.json() + console.log('📦 Leaderboard data received:', { + success: data.success, + completed_count: data.leaderboard?.length || 0, + waiting_count: data.waiting_participants?.length || 0, + ultimate_fix_applied: data.ultimate_fix_applied + }) + if (data.success) { setLeaderboard(data.leaderboard || []) setWaitingParticipants(data.waiting_participants || []) setExamStats(data.stats || {}) - // ✅ FIXED TIMER CALCULATION + // Timer calculation if (data.exam_info && data.exam_info.status === 'active') { if (data.exam_info.end_time) { const now = Date.now() const endTime = new Date(data.exam_info.end_time).getTime() const remaining = Math.max(0, Math.floor((endTime - now) / 1000)) - console.log(`⏰ Timer calculation:`) - console.log(` Current: ${new Date(now).toISOString()}`) - console.log(` End: ${new Date(endTime).toISOString()}`) - console.log(` Remaining: ${remaining} seconds`) - setTimeRemaining(remaining) if (!timerInitialized) { setTimerInitialized(true) } - } else if (data.exam_info.start_time && data.exam_info.duration_minutes) { - // Calculate from start_time + duration - const startTime = new Date(data.exam_info.start_time).getTime() - const durationMs = data.exam_info.duration_minutes * 60 * 1000 - const endTime = startTime + durationMs - const now = Date.now() - const remaining = Math.max(0, Math.floor((endTime - now) / 1000)) - - console.log(`⏰ Using start_time + duration - Remaining: ${remaining}s`) - setTimeRemaining(remaining) - if (!timerInitialized) { - setTimerInitialized(true) - } - } - } else if (data.exam_info && data.exam_info.status === 'waiting') { - // Show full duration for waiting exams - const fullSeconds = (data.exam_info.duration_minutes || 30) * 60 - setTimeRemaining(fullSeconds) - if (!timerInitialized) { - setTimerInitialized(true) } } + + // ✅ ENHANCED: Better user status checking + const currentUser = examSession?.student_name + if (currentUser) { + const userInCompleted = data.leaderboard.find((p: Participant) => p.name === currentUser) + const userInWaiting = data.waiting_participants.find((p: Participant) => p.name === currentUser) + + console.log(`👤 User status check:`, { + username: currentUser, + in_completed: !!userInCompleted, + in_waiting: !!userInWaiting, + current_hasSubmitted: hasSubmitted, + user_score: userInCompleted?.score + }) + + if (userInCompleted && !hasSubmitted) { + console.log('✅ User found in completed leaderboard, updating hasSubmitted state') + setHasSubmitted(true) + } + } + + // Debug logging for leaderboard content + if (data.leaderboard.length > 0) { + console.log('🏆 Completed participants:', data.leaderboard.map((p: any) => `${p.name}: ${p.score}%`)) + } + if (data.waiting_participants.length > 0) { + console.log('⏳ Waiting participants:', data.waiting_participants.map((p: any) => p.name)) + } + + } else { + console.error('❌ Leaderboard fetch failed:', data.error) } } catch (error) { - console.error('Failed to fetch leaderboard:', error) + console.error('❌ Failed to fetch leaderboard:', error) } } @@ -177,7 +191,6 @@ export default function EnhancedExamInterface() { setTestResults([]) } - // ✅ FIXED RUNCODE FUNCTION - Updated to use correct endpoint const runCode = async () => { if (!code.trim()) { alert('Please write some code first!') @@ -189,90 +202,283 @@ export default function EnhancedExamInterface() { setTestResults([]) try { - console.log('🔧 Sending code to compiler...') - - // ✅ FIXED: Use correct endpoint /api/compiler/execute instead of /api/exam/execute-code const response = await fetch('http://127.0.0.1:5000/api/compiler/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - language: selectedLanguage, - code: code, - input: '' + code, + language: selectedLanguage }) }) const result = await response.json() - console.log('📦 Compiler result:', result) if (result.success) { - setOutput(`✅ Code executed successfully!\n${result.output}`) + setOutput(`✅ Output:\n${result.output}`) if (result.execution_time) { setOutput(prev => prev + `\n⏱️ Execution time: ${result.execution_time}s`) } - if (result.error) { - setOutput(prev => prev + `\n⚠️ Warnings:\n${result.error}`) - } - - // If there are test results from backend, show them - if (result.test_results) { - setTestResults(result.test_results) - } } else { setOutput(`❌ Error:\n${result.error}`) } } catch (error) { - console.error('❌ Compiler network error:', error) - setOutput(`❌ Network error: Could not connect to compiler service.\nPlease check if the backend is running on port 5000.`) + setOutput(`Execution failed: ${(error as Error).message}`) } finally { setIsRunning(false) } } + // ✅ COMPLETELY FIXED SUBMIT SOLUTION with aggressive leaderboard refresh const submitSolution = async () => { if (!code.trim()) { alert('Please write some code before submitting!') return } + if (!confirm('Submit your solution? This cannot be undone.')) return + setIsSubmitting(true) try { + console.log('📤 Submitting solution...') + console.log('👤 Participant:', examSession?.student_name) + console.log('🔢 Exam Code:', examSession?.exam_code) + const response = await fetch('http://127.0.0.1:5000/api/exam/submit-solution', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ exam_code: examSession?.exam_code, language: selectedLanguage, - code: code + code: code, + participant_name: examSession?.student_name || 'Anonymous' }) }) const data = await response.json() + console.log('📦 Submit result:', data) if (data.success) { setHasSubmitted(true) setTestResults(data.test_results || []) - let alertMessage = `Solution submitted successfully!\nScore: ${data.score}%\nPassed: ${data.passed_tests}/${data.total_tests} tests` + // ✅ ENHANCED: Detailed alert with proper test results formatting + let alertMessage = `🎉 Solution submitted successfully!\n\n` + alertMessage += `📊 Overall Score: ${data.score}%\n` + alertMessage += `✅ Tests Passed: ${data.passed_tests}/${data.total_tests}\n` - if (data.blockchain_verified) { - alertMessage += `\n🔗 Blockchain Verified: ${data.wallet_address?.slice(0, 6)}...${data.wallet_address?.slice(-4)}` + if (data.execution_time) { + alertMessage += `⏱️ Execution Time: ${data.execution_time}s\n` } + // Enhanced test results display in alert + if (data.test_results && data.test_results.length > 0) { + alertMessage += `\n📋 Detailed Test Results:\n` + alertMessage += `${'='.repeat(30)}\n` + + data.test_results.forEach((test: any, i: number) => { + const status = test.passed ? '✅ PASSED' : '❌ FAILED' + const points = test.points_earned || 0 + + alertMessage += `Test ${i+1}: ${status} (+${points} points)\n` + + if (test.description && test.description !== `Test case ${i+1}`) { + alertMessage += ` Description: ${test.description}\n` + } + + if (test.input) { + alertMessage += ` Input: "${test.input}"\n` + } + + if (test.expected_output) { + alertMessage += ` Expected: "${test.expected_output}"\n` + } + + if (test.actual_output) { + alertMessage += ` Your Output: "${test.actual_output}"\n` + } + + if (!test.passed && test.error) { + alertMessage += ` Error: ${test.error}\n` + } + + alertMessage += `\n` + }) + + // Add summary + const totalPoints = data.test_results.reduce((sum: number, test: any) => sum + (test.points_earned || 0), 0) + const maxPoints = data.scoring_details?.total_points || 100 + alertMessage += `📈 Points Earned: ${totalPoints}/${maxPoints}\n` + } + + alertMessage += `\n🏆 Your score will appear in the leaderboard shortly!` + alert(alertMessage) - fetchLeaderboard(examSession!.exam_code) + + // ✅ CRITICAL FIX: Aggressive leaderboard refresh sequence + console.log('🔄 Starting aggressive leaderboard refresh sequence...') + + // Immediate refresh + setTimeout(() => { + console.log('🔄 Refresh 1/6 - Immediate') + fetchLeaderboard(examSession!.exam_code) + }, 200) + + // Quick follow-up + setTimeout(() => { + console.log('🔄 Refresh 2/6 - Quick follow-up') + fetchLeaderboard(examSession!.exam_code) + }, 800) + + // Medium delay + setTimeout(() => { + console.log('🔄 Refresh 3/6 - Medium delay') + fetchLeaderboard(examSession!.exam_code) + }, 2000) + + // Longer delay + setTimeout(() => { + console.log('🔄 Refresh 4/6 - Longer delay') + fetchLeaderboard(examSession!.exam_code) + }, 4000) + + // Extended delay + setTimeout(() => { + console.log('🔄 Refresh 5/6 - Extended delay') + fetchLeaderboard(examSession!.exam_code) + }, 7000) + + // Final refresh + setTimeout(() => { + console.log('🔄 Refresh 6/6 - Final check') + fetchLeaderboard(examSession!.exam_code) + }, 10000) + } else { - alert(data.error || 'Failed to submit solution') + alert(`❌ Submission failed: ${data.error}`) } + } catch (error) { - alert('Failed to submit solution. Please try again.') + console.error('❌ Submit network error:', error) + alert('❌ Network error: Could not submit solution. Please try again.') } finally { setIsSubmitting(false) } } - // ✅ FIXED TIME FORMATTING + // ✅ Enhanced Test Results Display Component + const TestResultsDisplay = ({ results }: { results: any[] }) => { + if (!results || results.length === 0) return null + + return ( +
+

+ + Test Results +

+ +
+ {results.map((result, index) => ( +
+
+
+ + Test {index + 1}: {result.passed ? '✅ PASSED' : '❌ FAILED'} + + + +{result.points_earned || 0} points + +
+
+ + {result.description && result.description !== `Test case ${index+1}` && ( +

{result.description}

+ )} + +
+ {result.input && ( +
+ Input: + + "{result.input}" + +
+ )} + + {result.expected_output && ( +
+ Expected: + + "{result.expected_output}" + +
+ )} + + {result.actual_output && ( +
+ Your Output: + + "{result.actual_output}" + +
+ )} +
+ + {!result.passed && result.error && ( +
+ Error: {result.error} +
+ )} +
+ ))} +
+ + {/* Summary */} +
+
+ + Passed: {results.filter(r => r.passed).length}/{results.length} tests + + + Points: {results.reduce((sum, r) => sum + (r.points_earned || 0), 0)} total + +
+
+
+ ) + } + + // Debug function for troubleshooting + const debugLeaderboard = async () => { + try { + const response = await fetch(`http://127.0.0.1:5000/api/exam/leaderboard/${examSession?.exam_code}`) + const data = await response.json() + + console.log('🐛 DEBUG LEADERBOARD:', { + success: data.success, + completed_count: data.leaderboard?.length || 0, + waiting_count: data.waiting_participants?.length || 0, + my_name: examSession?.student_name, + in_completed: data.leaderboard?.find((p: any) => p.name === examSession?.student_name), + in_waiting: data.waiting_participants?.find((p: any) => p.name === examSession?.student_name), + ultimate_fix_applied: data.ultimate_fix_applied, + full_leaderboard: data.leaderboard, + full_waiting: data.waiting_participants + }) + + alert(`Debug Info:\nCompleted: ${data.leaderboard?.length || 0}\nWaiting: ${data.waiting_participants?.length || 0}\nCheck console for details`) + } catch (error) { + console.error('Debug error:', error) + } + } + const formatTime = (seconds: number) => { if (seconds < 0) return "00:00" @@ -308,11 +514,11 @@ export default function EnhancedExamInterface() {

{problem.title}

-

Code: {examSession.exam_code}

+

Code: {examSession.exam_code} | Participant: {examSession.student_name}

- {/* ✅ FIXED TIMER DISPLAY */} + {/* Timer */} {timeRemaining > 0 && (
)} - {/* Wallet Info Display */} - {examSession.blockchain_verified && examSession.wallet_address && ( -
- - - {examSession.wallet_address.slice(0, 6)}...{examSession.wallet_address.slice(-4)} - - -
- )} - {/* Participant Count */}
{examStats.total_participants || 0} participants - {examStats.blockchain_participants > 0 && ( - - ({examStats.blockchain_participants} 🔗) - - )}
+ + {/* Submission Status Indicator */} + {hasSubmitted && ( +
+ + ✅ Submitted +
+ )}
@@ -360,10 +558,10 @@ export default function EnhancedExamInterface() {

{problem.title}

- {examSession.blockchain_verified && ( + {hasSubmitted && (
- Blockchain Verified + Solution Submitted
)}
@@ -428,7 +626,7 @@ export default function EnhancedExamInterface() { className="w-full h-64 bg-gray-900 text-green-400 font-mono p-4 rounded border border-gray-600 resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" disabled={hasSubmitted} spellCheck={false} - placeholder={`Write your ${selectedLanguage} solution here...`} + placeholder={hasSubmitted ? 'Solution submitted!' : `Write your ${selectedLanguage} solution here...`} />
@@ -436,10 +634,7 @@ export default function EnhancedExamInterface() { Function: {problem.function_name} {hasSubmitted && ( - ✅ Solution submitted - {examSession.blockchain_verified && ( - 🔗 Blockchain verified - )} + ✅ Solution submitted successfully! )}
@@ -455,63 +650,32 @@ export default function EnhancedExamInterface() {
- {/* Output & Test Results */} - {(output || testResults.length > 0) && ( + {/* Output Display */} + {output && (
- {output && ( -
-

Output:

-
{output}
-
- )} - - {testResults.length > 0 && ( -
-

Test Results:

-
- {testResults.map((result, index) => ( -
-
-
- - Test {index + 1}: {result.passed ? '✅ Passed' : '❌ Failed'} - - {result.input && ( -
- Input: "{result.input}" -
- )} -
- {!result.passed && result.error && ( - {result.error} - )} -
-
- ))} -
-
- )} +

Output:

+
{output}
)} + + {/* ✅ Enhanced Test Results Display */} + {testResults.length > 0 && ( + + )} - {/* Leaderboard */} + {/* Enhanced Leaderboard */}
@@ -519,13 +683,24 @@ export default function EnhancedExamInterface() {

Live Leaderboard

- +
+ + + {/* Debug button - remove in production */} + +
{/* Stats */} @@ -548,20 +723,7 @@ export default function EnhancedExamInterface() {
- {/* Blockchain Stats */} - {examStats.blockchain_participants > 0 && ( -
-
-
-
{examStats.blockchain_participants}
-
Blockchain Verified
-
- -
-
- )} - - {/* Leaderboard */} + {/* Leaderboard Display */}

🏆 Rankings

{leaderboard.length > 0 ? ( @@ -571,12 +733,9 @@ export default function EnhancedExamInterface() {
#{participant.rank}
-
+
{participant.name} - {participant.name === examSession.student_name && ' (You)'} - {participant.blockchain_verified && ( - - )} + {participant.name === examSession.student_name && ' (You) 🎯'}
{participant.language && ( @@ -584,15 +743,15 @@ export default function EnhancedExamInterface() { {languageIcons[participant.language]} {participant.language} )} - {participant.wallet_short && ( - - {participant.wallet_short} - - )}
- {participant.score}% +
+ {participant.score}% +
+ Submitted ✅ +
+
)) @@ -614,9 +773,7 @@ export default function EnhancedExamInterface() { {participant.name} {participant.name === examSession.student_name && ' (You)'} - {participant.blockchain_verified && ( - - )} + Working... ))} diff --git a/frontend/app/coding/host/[examCode]/page.tsx b/frontend/app/coding/host/[examCode]/page.tsx index cf18a91..bab1a67 100644 --- a/frontend/app/coding/host/[examCode]/page.tsx +++ b/frontend/app/coding/host/[examCode]/page.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { useRouter, useParams } from 'next/navigation' import { Users, Trophy, Clock, Play, Square, RefreshCw, Settings, @@ -40,6 +40,7 @@ interface Question { interface ExamInfo { title: string + exam_code: string status: 'waiting' | 'active' | 'completed' duration_minutes: number participants_count: number @@ -49,6 +50,9 @@ interface ExamInfo { languages: string[] created_at: string host_name: string + start_time?: string + end_time?: string + problem?: Question } interface Participant { @@ -61,6 +65,20 @@ interface Participant { total_tests?: number points_earned?: number total_points?: number + language?: string + rank?: number +} + +interface LeaderboardData { + leaderboard: Participant[] + waiting_participants: Participant[] + stats: { + total_participants: number + completed_submissions: number + waiting_submissions: number + average_score: number + highest_score: number + } } /* ---------- Enhanced Host Panel Component ---------- */ @@ -71,7 +89,17 @@ export default function EnhancedHostPanel() { /* ------- Global state ------- */ const [examInfo, setExamInfo] = useState(null) - const [participants, setParticipants] = useState([]) + const [leaderboardData, setLeaderboardData] = useState({ + leaderboard: [], + waiting_participants: [], + stats: { + total_participants: 0, + completed_submissions: 0, + waiting_submissions: 0, + average_score: 0, + highest_score: 0 + } + }) const [loading, setLoading] = useState(true) const [error, setError] = useState('') @@ -98,7 +126,7 @@ export default function EnhancedHostPanel() { expected_output: '', description: 'Test case 1', is_public: true, - points: 25 + points: 100 }], examples: [{ input: '', @@ -118,6 +146,44 @@ export default function EnhancedHostPanel() { } const [draft, setDraft] = useState({ ...blankQuestion }) + /* ------------------------------------------------------------------- */ + /* FIXED EVENT HANDLERS */ + /* ------------------------------------------------------------------- */ + + // ✅ FIXED: Stable event handlers using useCallback + const handleTitleChange = useCallback((e: React.ChangeEvent) => { + setDraft(prev => ({...prev, title: e.target.value})) + }, []) + + const handleDescriptionChange = useCallback((e: React.ChangeEvent) => { + setDraft(prev => ({...prev, description: e.target.value})) + }, []) + + const handleDifficultyChange = useCallback((e: React.ChangeEvent) => { + setDraft(prev => ({...prev, difficulty: e.target.value as any})) + }, []) + + const handleTotalPointsChange = useCallback((e: React.ChangeEvent) => { + const newTotal = parseInt(e.target.value) || 100 + setDraft(prev => ({...prev, total_points: newTotal})) + }, []) + + const handleCorrectSolutionChange = useCallback((e: React.ChangeEvent) => { + setDraft(prev => ({ + ...prev, + correct_solution: {...prev.correct_solution, python: e.target.value} + })) + }, []) + + const handleExampleChange = useCallback((index: number, field: keyof Example, value: string) => { + setDraft(prev => ({ + ...prev, + examples: prev.examples.map((ex, i) => + i === index ? {...ex, [field]: value} : ex + ) + })) + }, []) + /* ------------------------------------------------------------------- */ /* API CALLS */ /* ------------------------------------------------------------------- */ @@ -128,32 +194,48 @@ export default function EnhancedHostPanel() { const data = await res.json() if (data.success) { setExamInfo(data.exam_info) - setCustomDuration(data.exam_info.duration_minutes) + setCustomDuration(data.exam_info.duration_minutes || 30) setError('') } else { setError(data.error || 'Unable to load exam') } - } catch { + } catch (err) { setError('Backend unreachable') + console.error('Failed to fetch exam info:', err) } finally { setLoading(false) } } - const fetchParticipants = async () => { + const fetchLeaderboard = async () => { try { - const res = await fetch(`http://127.0.0.1:5000/api/exam/participants/${examCode}`) + const res = await fetch(`http://127.0.0.1:5000/api/exam/leaderboard/${examCode}`) const data = await res.json() - if (data.success) setParticipants(data.participants) - } catch { - /** ignore */ + if (data.success) { + setLeaderboardData({ + leaderboard: data.leaderboard || [], + waiting_participants: data.waiting_participants || [], + stats: data.stats || { + total_participants: 0, + completed_submissions: 0, + waiting_submissions: 0, + average_score: 0, + highest_score: 0 + } + }) + } + } catch (err) { + console.error('Failed to fetch leaderboard:', err) } } useEffect(() => { fetchExamInfo() - fetchParticipants() - // eslint-disable-next-line react-hooks/exhaustive-deps + fetchLeaderboard() + + // Poll leaderboard every 3 seconds for real-time updates + const interval = setInterval(fetchLeaderboard, 3000) + return () => clearInterval(interval) }, [examCode]) /* ---------- Enhanced Question Upload ---------- */ @@ -185,7 +267,8 @@ export default function EnhancedHostPanel() { const enhancedQuestion = { ...draft, test_cases: validTestCases, - id: Date.now().toString() + id: Date.now().toString(), + languages: Object.keys(draft.starter_code).filter(lang => draft.starter_code[lang].trim()) } const res = await fetch('http://127.0.0.1:5000/api/exam/upload-question', { @@ -199,14 +282,15 @@ export default function EnhancedHostPanel() { const data = await res.json() if (data.success) { - alert(`✅ Enhanced question uploaded with ${validTestCases.length} test cases!`) + alert(`✅ Enhanced question uploaded with ${validTestCases.length} test cases!\nTotal points: ${draft.total_points}`) setShowUploader(false) setDraft({ ...blankQuestion }) fetchExamInfo() } else { alert(`❌ ${data.error}`) } - } catch { + } catch (err) { + console.error('Upload error:', err) alert('❌ Network error') } } @@ -226,14 +310,14 @@ export default function EnhancedHostPanel() { })) } - const updateTestCase = (index: number, field: keyof TestCase, value: any) => { + const updateTestCase = useCallback((index: number, field: keyof TestCase, value: any) => { setDraft(prev => ({ ...prev, test_cases: prev.test_cases.map((tc, i) => i === index ? { ...tc, [field]: value } : tc ) })) - } + }, []) const removeTestCase = (index: number) => { if (draft.test_cases.length <= 1) { @@ -246,38 +330,173 @@ export default function EnhancedHostPanel() { })) } - /* ---------- Duration Update ---------- */ - const updateDuration = async () => { - if (customDuration < 5) { - alert('Minimum 5 minutes') - return + // Auto-distribute points when total points change + const redistributePoints = () => { + const pointsPerTest = Math.floor(draft.total_points / draft.test_cases.length) + const remainder = draft.total_points % draft.test_cases.length + + setDraft(prev => ({ + ...prev, + test_cases: prev.test_cases.map((tc, index) => ({ + ...tc, + points: pointsPerTest + (index < remainder ? 1 : 0) + })) + })) + } + + /* ---------- Exam Control Functions ---------- */ + const startExam = async () => { + if (!examInfo?.problem_title) { + alert('Please upload a question before starting the exam') + return } + + if (!confirm('Start the exam now? Participants will be able to submit solutions.')) return + try { - const res = await fetch('http://127.0.0.1:5000/api/exam/update-duration', { + const res = await fetch('http://127.0.0.1:5000/api/exam/start-exam', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ exam_code: examCode, duration_minutes: customDuration }) + body: JSON.stringify({ exam_code: examCode }) }) const data = await res.json() + if (data.success) { - alert('✅ Duration updated') - setShowDurationEdit(false) + alert('✅ Exam started successfully!') fetchExamInfo() - } else alert(`❌ ${data.error}`) - } catch { - alert('❌ Network error') + } else { + alert(`❌ ${data.error}`) + } + } catch (err) { + console.error('Start exam error:', err) + alert('❌ Network error') } } - /* ---------- Enhanced Question Upload Form ---------- */ + const stopExam = async () => { + if (!confirm('Stop the exam immediately? This will end the exam for all participants.')) return + + try { + const res = await fetch('http://127.0.0.1:5000/api/exam/stop-exam', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ exam_code: examCode }) + }) + const data = await res.json() + + if (data.success) { + alert('✅ Exam stopped successfully!') + fetchExamInfo() + } else { + alert(`❌ ${data.error}`) + } + } catch (err) { + console.error('Stop exam error:', err) + alert('❌ Network error') + } + } + + /* ---------- FIXED Test Case Editor Component ---------- */ + const TestCaseEditor = React.memo(({ + testCase, + index, + onUpdate, + onRemove, + canRemove + }: { + testCase: TestCase + index: number + onUpdate: (index: number, field: keyof TestCase, value: any) => void + onRemove: (index: number) => void + canRemove: boolean + }) => { + const handleInputChange = useCallback((field: keyof TestCase, value: any) => { + onUpdate(index, field, value) + }, [index, onUpdate]) + + return ( +
+
+ Test Case {index + 1} +
+ + handleInputChange('points', parseInt(e.target.value) || 0)} + className="w-20 p-1 bg-gray-700 rounded text-sm border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none" + placeholder="Points" + min="0" + autoComplete="off" + /> + {canRemove && ( + + )} +
+
+ +
+
+ +