mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
update & add
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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}"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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}}
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
@@ -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/<course_id>", 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/<course_id>", 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/<course_id>/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/<course_id>/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/<id>",
|
||||||
|
"DELETE /api/admin/courses/<id>",
|
||||||
|
"POST /api/admin/initialize",
|
||||||
|
"GET /api/admin/test",
|
||||||
|
"GET /api/admin/stats"
|
||||||
|
]
|
||||||
|
})
|
||||||
@@ -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
|
||||||
@@ -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/<user_id>', 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"
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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, [])
|
||||||
@@ -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
|
||||||
@@ -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("/<course_id>", 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("/<course_id>/lessons/<lesson_id>", methods=["GET"])
|
||||||
|
def get_lesson(course_id, lesson_id):
|
||||||
|
"""Get specific lesson content - DYNAMIC"""
|
||||||
|
try:
|
||||||
|
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"_id": 0})
|
||||||
|
|
||||||
|
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("/<course_id>/lessons/<lesson_id>/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("/<course_id>/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
|
||||||
@@ -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/<user_id>', 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": []
|
||||||
|
})
|
||||||
@@ -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/<exam_code>", 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/<exam_code>", 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/<exam_code>", 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/<exam_code>', 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/<exam_code>', 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/<exam_code>",
|
||||||
|
"/api/exam/get-problem/<exam_code>",
|
||||||
|
"/api/exam/host-dashboard/<exam_code>",
|
||||||
|
"/api/exam/info/<exam_code>",
|
||||||
|
"/api/exam/participants/<exam_code>",
|
||||||
|
"/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/<exam_code>",
|
||||||
|
"/api/exam/get-problem/<exam_code>",
|
||||||
|
"/api/exam/host-dashboard/<exam_code>",
|
||||||
|
"/api/exam/info/<exam_code>",
|
||||||
|
"/api/exam/participants/<exam_code>",
|
||||||
|
"/api/exam/remove-participant",
|
||||||
|
"/api/exam/stop-exam",
|
||||||
|
"/api/exam/test",
|
||||||
|
"/api/exam/debug-join-data"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
@bp.route('/info/<exam_code>', 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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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())
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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'))
|
||||||
@@ -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
|
||||||
@@ -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 {}
|
||||||
+212
-580
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
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 {
|
interface Participant {
|
||||||
name: string
|
name: string
|
||||||
@@ -53,9 +53,7 @@ export default function EnhancedExamInterface() {
|
|||||||
const languageIcons: {[key: string]: string} = {
|
const languageIcons: {[key: string]: string} = {
|
||||||
python: '🐍',
|
python: '🐍',
|
||||||
java: '☕',
|
java: '☕',
|
||||||
javascript: '🌐',
|
c: '⚡',
|
||||||
cpp: '⚡',
|
|
||||||
c: '🔧',
|
|
||||||
bash: '💻'
|
bash: '💻'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,15 +70,15 @@ export default function EnhancedExamInterface() {
|
|||||||
// Fetch problem details
|
// Fetch problem details
|
||||||
fetchProblem(session.exam_code)
|
fetchProblem(session.exam_code)
|
||||||
|
|
||||||
// Start polling for updates
|
// More frequent polling for real-time updates
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchLeaderboard(session.exam_code)
|
fetchLeaderboard(session.exam_code)
|
||||||
}, 3000)
|
}, 2000)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [router])
|
}, [router])
|
||||||
|
|
||||||
// ✅ FIXED TIMER COUNTDOWN
|
// Timer countdown
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!timerInitialized || timeRemaining <= 0) return
|
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) => {
|
const fetchLeaderboard = async (examCode: string) => {
|
||||||
try {
|
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()
|
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) {
|
if (data.success) {
|
||||||
setLeaderboard(data.leaderboard || [])
|
setLeaderboard(data.leaderboard || [])
|
||||||
setWaitingParticipants(data.waiting_participants || [])
|
setWaitingParticipants(data.waiting_participants || [])
|
||||||
setExamStats(data.stats || {})
|
setExamStats(data.stats || {})
|
||||||
|
|
||||||
// ✅ FIXED TIMER CALCULATION
|
// Timer calculation
|
||||||
if (data.exam_info && data.exam_info.status === 'active') {
|
if (data.exam_info && data.exam_info.status === 'active') {
|
||||||
if (data.exam_info.end_time) {
|
if (data.exam_info.end_time) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const endTime = new Date(data.exam_info.end_time).getTime()
|
const endTime = new Date(data.exam_info.end_time).getTime()
|
||||||
const remaining = Math.max(0, Math.floor((endTime - now) / 1000))
|
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)
|
setTimeRemaining(remaining)
|
||||||
if (!timerInitialized) {
|
if (!timerInitialized) {
|
||||||
setTimerInitialized(true)
|
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
|
// ✅ ENHANCED: Better user status checking
|
||||||
setTimeRemaining(fullSeconds)
|
const currentUser = examSession?.student_name
|
||||||
if (!timerInitialized) {
|
if (currentUser) {
|
||||||
setTimerInitialized(true)
|
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) {
|
} 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([])
|
setTestResults([])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ FIXED RUNCODE FUNCTION - Updated to use correct endpoint
|
|
||||||
const runCode = async () => {
|
const runCode = async () => {
|
||||||
if (!code.trim()) {
|
if (!code.trim()) {
|
||||||
alert('Please write some code first!')
|
alert('Please write some code first!')
|
||||||
@@ -189,90 +202,283 @@ export default function EnhancedExamInterface() {
|
|||||||
setTestResults([])
|
setTestResults([])
|
||||||
|
|
||||||
try {
|
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', {
|
const response = await fetch('http://127.0.0.1:5000/api/compiler/execute', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
language: selectedLanguage,
|
code,
|
||||||
code: code,
|
language: selectedLanguage
|
||||||
input: ''
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
console.log('📦 Compiler result:', result)
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setOutput(`✅ Code executed successfully!\n${result.output}`)
|
setOutput(`✅ Output:\n${result.output}`)
|
||||||
if (result.execution_time) {
|
if (result.execution_time) {
|
||||||
setOutput(prev => prev + `\n⏱️ Execution time: ${result.execution_time}s`)
|
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 {
|
} else {
|
||||||
setOutput(`❌ Error:\n${result.error}`)
|
setOutput(`❌ Error:\n${result.error}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Compiler network error:', error)
|
setOutput(`Execution failed: ${(error as Error).message}`)
|
||||||
setOutput(`❌ Network error: Could not connect to compiler service.\nPlease check if the backend is running on port 5000.`)
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsRunning(false)
|
setIsRunning(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ COMPLETELY FIXED SUBMIT SOLUTION with aggressive leaderboard refresh
|
||||||
const submitSolution = async () => {
|
const submitSolution = async () => {
|
||||||
if (!code.trim()) {
|
if (!code.trim()) {
|
||||||
alert('Please write some code before submitting!')
|
alert('Please write some code before submitting!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!confirm('Submit your solution? This cannot be undone.')) return
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
try {
|
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', {
|
const response = await fetch('http://127.0.0.1:5000/api/exam/submit-solution', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
exam_code: examSession?.exam_code,
|
exam_code: examSession?.exam_code,
|
||||||
language: selectedLanguage,
|
language: selectedLanguage,
|
||||||
code: code
|
code: code,
|
||||||
|
participant_name: examSession?.student_name || 'Anonymous'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
console.log('📦 Submit result:', data)
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setHasSubmitted(true)
|
setHasSubmitted(true)
|
||||||
setTestResults(data.test_results || [])
|
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) {
|
if (data.execution_time) {
|
||||||
alertMessage += `\n🔗 Blockchain Verified: ${data.wallet_address?.slice(0, 6)}...${data.wallet_address?.slice(-4)}`
|
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)
|
alert(alertMessage)
|
||||||
|
|
||||||
|
// ✅ 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)
|
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 {
|
} else {
|
||||||
alert(data.error || 'Failed to submit solution')
|
alert(`❌ Submission failed: ${data.error}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (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 {
|
} finally {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ FIXED TIME FORMATTING
|
// ✅ Enhanced Test Results Display Component
|
||||||
|
const TestResultsDisplay = ({ results }: { results: any[] }) => {
|
||||||
|
if (!results || results.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 bg-gray-900 p-4 rounded border border-gray-600">
|
||||||
|
<h4 className="text-lg font-semibold text-white mb-4 flex items-center space-x-2">
|
||||||
|
<TestTube className="h-5 w-5 text-blue-400" />
|
||||||
|
<span>Test Results</span>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{results.map((result, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`p-4 rounded-lg border-l-4 ${
|
||||||
|
result.passed
|
||||||
|
? 'bg-green-900 border-green-500 text-green-100'
|
||||||
|
: 'bg-red-900 border-red-500 text-red-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="font-semibold">
|
||||||
|
Test {index + 1}: {result.passed ? '✅ PASSED' : '❌ FAILED'}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm bg-black bg-opacity-30 px-2 py-1 rounded font-bold">
|
||||||
|
+{result.points_earned || 0} points
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result.description && result.description !== `Test case ${index+1}` && (
|
||||||
|
<p className="text-sm mb-2 opacity-80">{result.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
||||||
|
{result.input && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Input:</span>
|
||||||
|
<code className="ml-2 bg-black bg-opacity-30 px-2 py-1 rounded">
|
||||||
|
"{result.input}"
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result.expected_output && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Expected:</span>
|
||||||
|
<code className="ml-2 bg-black bg-opacity-30 px-2 py-1 rounded">
|
||||||
|
"{result.expected_output}"
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result.actual_output && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Your Output:</span>
|
||||||
|
<code className="ml-2 bg-black bg-opacity-30 px-2 py-1 rounded">
|
||||||
|
"{result.actual_output}"
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!result.passed && result.error && (
|
||||||
|
<div className="mt-2 p-2 bg-red-800 bg-opacity-50 rounded text-sm">
|
||||||
|
<span className="font-medium">Error:</span> {result.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
<div className="mt-4 p-3 bg-blue-900 bg-opacity-50 rounded">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span>
|
||||||
|
Passed: {results.filter(r => r.passed).length}/{results.length} tests
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Points: {results.reduce((sum, r) => sum + (r.points_earned || 0), 0)} total
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) => {
|
const formatTime = (seconds: number) => {
|
||||||
if (seconds < 0) return "00:00"
|
if (seconds < 0) return "00:00"
|
||||||
|
|
||||||
@@ -308,11 +514,11 @@ export default function EnhancedExamInterface() {
|
|||||||
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold">{problem.title}</h1>
|
<h1 className="text-xl font-bold">{problem.title}</h1>
|
||||||
<p className="text-gray-400">Code: {examSession.exam_code}</p>
|
<p className="text-gray-400">Code: {examSession.exam_code} | Participant: {examSession.student_name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
{/* ✅ FIXED TIMER DISPLAY */}
|
{/* Timer */}
|
||||||
{timeRemaining > 0 && (
|
{timeRemaining > 0 && (
|
||||||
<div className={`flex items-center space-x-2 px-3 py-1 rounded-lg ${
|
<div className={`flex items-center space-x-2 px-3 py-1 rounded-lg ${
|
||||||
timeRemaining <= 300 ? 'bg-red-900' : timeRemaining <= 600 ? 'bg-yellow-900' : 'bg-green-900'
|
timeRemaining <= 300 ? 'bg-red-900' : timeRemaining <= 600 ? 'bg-yellow-900' : 'bg-green-900'
|
||||||
@@ -328,27 +534,19 @@ export default function EnhancedExamInterface() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Wallet Info Display */}
|
|
||||||
{examSession.blockchain_verified && examSession.wallet_address && (
|
|
||||||
<div className="flex items-center space-x-2 bg-green-900 px-3 py-1 rounded-lg">
|
|
||||||
<Wallet className="h-4 w-4 text-green-400" />
|
|
||||||
<span className="text-green-200 text-sm font-mono">
|
|
||||||
{examSession.wallet_address.slice(0, 6)}...{examSession.wallet_address.slice(-4)}
|
|
||||||
</span>
|
|
||||||
<Shield className="h-4 w-4 text-green-400" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Participant Count */}
|
{/* Participant Count */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Users className="h-5 w-5 text-blue-400" />
|
<Users className="h-5 w-5 text-blue-400" />
|
||||||
<span>{examStats.total_participants || 0} participants</span>
|
<span>{examStats.total_participants || 0} participants</span>
|
||||||
{examStats.blockchain_participants > 0 && (
|
|
||||||
<span className="text-green-400 text-sm">
|
|
||||||
({examStats.blockchain_participants} 🔗)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Submission Status Indicator */}
|
||||||
|
{hasSubmitted && (
|
||||||
|
<div className="flex items-center space-x-2 bg-green-900 px-3 py-1 rounded-lg">
|
||||||
|
<Shield className="h-4 w-4 text-green-400" />
|
||||||
|
<span className="text-green-200 text-sm">✅ Submitted</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,10 +558,10 @@ export default function EnhancedExamInterface() {
|
|||||||
<div className="bg-gray-800 rounded-lg p-6">
|
<div className="bg-gray-800 rounded-lg p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-xl font-bold">{problem.title}</h2>
|
<h2 className="text-xl font-bold">{problem.title}</h2>
|
||||||
{examSession.blockchain_verified && (
|
{hasSubmitted && (
|
||||||
<div className="flex items-center space-x-1 text-green-400 text-sm">
|
<div className="flex items-center space-x-1 text-green-400 text-sm">
|
||||||
<Shield className="h-4 w-4" />
|
<Shield className="h-4 w-4" />
|
||||||
<span>Blockchain Verified</span>
|
<span>Solution Submitted</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -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"
|
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}
|
disabled={hasSubmitted}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder={`Write your ${selectedLanguage} solution here...`}
|
placeholder={hasSubmitted ? 'Solution submitted!' : `Write your ${selectedLanguage} solution here...`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-between items-center mt-4">
|
<div className="flex justify-between items-center mt-4">
|
||||||
@@ -436,10 +634,7 @@ export default function EnhancedExamInterface() {
|
|||||||
Function: <code className="text-blue-400">{problem.function_name}</code>
|
Function: <code className="text-blue-400">{problem.function_name}</code>
|
||||||
{hasSubmitted && (
|
{hasSubmitted && (
|
||||||
<span className="ml-4 text-green-400">
|
<span className="ml-4 text-green-400">
|
||||||
✅ Solution submitted
|
✅ Solution submitted successfully!
|
||||||
{examSession.blockchain_verified && (
|
|
||||||
<span className="ml-2">🔗 Blockchain verified</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -460,58 +655,27 @@ export default function EnhancedExamInterface() {
|
|||||||
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded flex items-center space-x-2 transition-colors"
|
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-4 py-2 rounded flex items-center space-x-2 transition-colors"
|
||||||
>
|
>
|
||||||
<Send className="h-4 w-4" />
|
<Send className="h-4 w-4" />
|
||||||
<span>{isSubmitting ? 'Submitting...' : hasSubmitted ? 'Submitted' : 'Submit Solution'}</span>
|
<span>{isSubmitting ? 'Submitting...' : hasSubmitted ? 'Submitted ✅' : 'Submit Solution'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Output & Test Results */}
|
{/* Output Display */}
|
||||||
{(output || testResults.length > 0) && (
|
|
||||||
<div className="mt-6 bg-gray-900 p-4 rounded">
|
|
||||||
{output && (
|
{output && (
|
||||||
<div className="mb-4">
|
<div className="mt-6 bg-gray-900 p-4 rounded">
|
||||||
<h4 className="text-sm font-medium text-gray-400 mb-2">Output:</h4>
|
<h4 className="text-sm font-medium text-gray-400 mb-2">Output:</h4>
|
||||||
<pre className="text-green-400 text-sm whitespace-pre-wrap">{output}</pre>
|
<pre className="text-green-400 text-sm whitespace-pre-wrap">{output}</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ✅ Enhanced Test Results Display */}
|
||||||
{testResults.length > 0 && (
|
{testResults.length > 0 && (
|
||||||
<div>
|
<TestResultsDisplay results={testResults} />
|
||||||
<h4 className="text-sm font-medium text-gray-400 mb-2">Test Results:</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{testResults.map((result, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`p-3 rounded text-sm ${
|
|
||||||
result.passed ? 'bg-green-900 text-green-200' : 'bg-red-900 text-red-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">
|
|
||||||
Test {index + 1}: {result.passed ? '✅ Passed' : '❌ Failed'}
|
|
||||||
</span>
|
|
||||||
{result.input && (
|
|
||||||
<div className="text-xs mt-1 opacity-75">
|
|
||||||
Input: "{result.input}"
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!result.passed && result.error && (
|
|
||||||
<span className="text-xs text-right">{result.error}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Leaderboard */}
|
{/* Enhanced Leaderboard */}
|
||||||
<div className="bg-gray-800 rounded-lg p-6">
|
<div className="bg-gray-800 rounded-lg p-6">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -519,6 +683,7 @@ export default function EnhancedExamInterface() {
|
|||||||
<h3 className="text-xl font-bold">Live Leaderboard</h3>
|
<h3 className="text-xl font-bold">Live Leaderboard</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => fetchLeaderboard(examSession.exam_code)}
|
onClick={() => fetchLeaderboard(examSession.exam_code)}
|
||||||
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded"
|
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded"
|
||||||
@@ -526,6 +691,16 @@ export default function EnhancedExamInterface() {
|
|||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Debug button - remove in production */}
|
||||||
|
<button
|
||||||
|
onClick={debugLeaderboard}
|
||||||
|
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded"
|
||||||
|
title="Debug"
|
||||||
|
>
|
||||||
|
🐛
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
@@ -548,20 +723,7 @@ export default function EnhancedExamInterface() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Blockchain Stats */}
|
{/* Leaderboard Display */}
|
||||||
{examStats.blockchain_participants > 0 && (
|
|
||||||
<div className="bg-green-900 p-3 rounded mb-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg font-bold text-green-200">{examStats.blockchain_participants}</div>
|
|
||||||
<div className="text-xs text-green-300">Blockchain Verified</div>
|
|
||||||
</div>
|
|
||||||
<Shield className="h-6 w-6 text-green-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Leaderboard */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="font-semibold text-gray-300 mb-3">🏆 Rankings</h4>
|
<h4 className="font-semibold text-gray-300 mb-3">🏆 Rankings</h4>
|
||||||
{leaderboard.length > 0 ? (
|
{leaderboard.length > 0 ? (
|
||||||
@@ -571,12 +733,9 @@ export default function EnhancedExamInterface() {
|
|||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<span className="font-bold text-lg">#{participant.rank}</span>
|
<span className="font-bold text-lg">#{participant.rank}</span>
|
||||||
<div>
|
<div>
|
||||||
<div className={`font-medium ${participant.name === examSession.student_name ? 'underline' : ''}`}>
|
<div className={`font-medium ${participant.name === examSession.student_name ? 'underline font-bold' : ''}`}>
|
||||||
{participant.name}
|
{participant.name}
|
||||||
{participant.name === examSession.student_name && ' (You)'}
|
{participant.name === examSession.student_name && ' (You) 🎯'}
|
||||||
{participant.blockchain_verified && (
|
|
||||||
<Shield className="inline h-3 w-3 ml-1 text-green-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs opacity-75 flex items-center space-x-2">
|
<div className="text-xs opacity-75 flex items-center space-x-2">
|
||||||
{participant.language && (
|
{participant.language && (
|
||||||
@@ -584,15 +743,15 @@ export default function EnhancedExamInterface() {
|
|||||||
{languageIcons[participant.language]} {participant.language}
|
{languageIcons[participant.language]} {participant.language}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{participant.wallet_short && (
|
|
||||||
<span className="font-mono text-green-300">
|
|
||||||
{participant.wallet_short}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
<span className="font-bold text-lg">{participant.score}%</span>
|
<span className="font-bold text-lg">{participant.score}%</span>
|
||||||
|
<div className="text-xs opacity-75">
|
||||||
|
Submitted ✅
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -614,9 +773,7 @@ export default function EnhancedExamInterface() {
|
|||||||
{participant.name}
|
{participant.name}
|
||||||
{participant.name === examSession.student_name && ' (You)'}
|
{participant.name === examSession.student_name && ' (You)'}
|
||||||
</span>
|
</span>
|
||||||
{participant.blockchain_verified && (
|
<span className="text-yellow-400 text-xs">Working...</span>
|
||||||
<Shield className="h-3 w-3 text-green-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import { useRouter, useParams } from 'next/navigation'
|
import { useRouter, useParams } from 'next/navigation'
|
||||||
import {
|
import {
|
||||||
Users, Trophy, Clock, Play, Square, RefreshCw, Settings,
|
Users, Trophy, Clock, Play, Square, RefreshCw, Settings,
|
||||||
@@ -40,6 +40,7 @@ interface Question {
|
|||||||
|
|
||||||
interface ExamInfo {
|
interface ExamInfo {
|
||||||
title: string
|
title: string
|
||||||
|
exam_code: string
|
||||||
status: 'waiting' | 'active' | 'completed'
|
status: 'waiting' | 'active' | 'completed'
|
||||||
duration_minutes: number
|
duration_minutes: number
|
||||||
participants_count: number
|
participants_count: number
|
||||||
@@ -49,6 +50,9 @@ interface ExamInfo {
|
|||||||
languages: string[]
|
languages: string[]
|
||||||
created_at: string
|
created_at: string
|
||||||
host_name: string
|
host_name: string
|
||||||
|
start_time?: string
|
||||||
|
end_time?: string
|
||||||
|
problem?: Question
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Participant {
|
interface Participant {
|
||||||
@@ -61,6 +65,20 @@ interface Participant {
|
|||||||
total_tests?: number
|
total_tests?: number
|
||||||
points_earned?: number
|
points_earned?: number
|
||||||
total_points?: 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 ---------- */
|
/* ---------- Enhanced Host Panel Component ---------- */
|
||||||
@@ -71,7 +89,17 @@ export default function EnhancedHostPanel() {
|
|||||||
|
|
||||||
/* ------- Global state ------- */
|
/* ------- Global state ------- */
|
||||||
const [examInfo, setExamInfo] = useState<ExamInfo | null>(null)
|
const [examInfo, setExamInfo] = useState<ExamInfo | null>(null)
|
||||||
const [participants, setParticipants] = useState<Participant[]>([])
|
const [leaderboardData, setLeaderboardData] = useState<LeaderboardData>({
|
||||||
|
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 [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
@@ -98,7 +126,7 @@ export default function EnhancedHostPanel() {
|
|||||||
expected_output: '',
|
expected_output: '',
|
||||||
description: 'Test case 1',
|
description: 'Test case 1',
|
||||||
is_public: true,
|
is_public: true,
|
||||||
points: 25
|
points: 100
|
||||||
}],
|
}],
|
||||||
examples: [{
|
examples: [{
|
||||||
input: '',
|
input: '',
|
||||||
@@ -118,6 +146,44 @@ export default function EnhancedHostPanel() {
|
|||||||
}
|
}
|
||||||
const [draft, setDraft] = useState<Question>({ ...blankQuestion })
|
const [draft, setDraft] = useState<Question>({ ...blankQuestion })
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------- */
|
||||||
|
/* FIXED EVENT HANDLERS */
|
||||||
|
/* ------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// ✅ FIXED: Stable event handlers using useCallback
|
||||||
|
const handleTitleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setDraft(prev => ({...prev, title: e.target.value}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setDraft(prev => ({...prev, description: e.target.value}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDifficultyChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setDraft(prev => ({...prev, difficulty: e.target.value as any}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleTotalPointsChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newTotal = parseInt(e.target.value) || 100
|
||||||
|
setDraft(prev => ({...prev, total_points: newTotal}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCorrectSolutionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
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 */
|
/* API CALLS */
|
||||||
/* ------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------- */
|
||||||
@@ -128,32 +194,48 @@ export default function EnhancedHostPanel() {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setExamInfo(data.exam_info)
|
setExamInfo(data.exam_info)
|
||||||
setCustomDuration(data.exam_info.duration_minutes)
|
setCustomDuration(data.exam_info.duration_minutes || 30)
|
||||||
setError('')
|
setError('')
|
||||||
} else {
|
} else {
|
||||||
setError(data.error || 'Unable to load exam')
|
setError(data.error || 'Unable to load exam')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
setError('Backend unreachable')
|
setError('Backend unreachable')
|
||||||
|
console.error('Failed to fetch exam info:', err)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchParticipants = async () => {
|
const fetchLeaderboard = async () => {
|
||||||
try {
|
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()
|
const data = await res.json()
|
||||||
if (data.success) setParticipants(data.participants)
|
if (data.success) {
|
||||||
} catch {
|
setLeaderboardData({
|
||||||
/** ignore */
|
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(() => {
|
useEffect(() => {
|
||||||
fetchExamInfo()
|
fetchExamInfo()
|
||||||
fetchParticipants()
|
fetchLeaderboard()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
|
// Poll leaderboard every 3 seconds for real-time updates
|
||||||
|
const interval = setInterval(fetchLeaderboard, 3000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
}, [examCode])
|
}, [examCode])
|
||||||
|
|
||||||
/* ---------- Enhanced Question Upload ---------- */
|
/* ---------- Enhanced Question Upload ---------- */
|
||||||
@@ -185,7 +267,8 @@ export default function EnhancedHostPanel() {
|
|||||||
const enhancedQuestion = {
|
const enhancedQuestion = {
|
||||||
...draft,
|
...draft,
|
||||||
test_cases: validTestCases,
|
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', {
|
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()
|
const data = await res.json()
|
||||||
|
|
||||||
if (data.success) {
|
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)
|
setShowUploader(false)
|
||||||
setDraft({ ...blankQuestion })
|
setDraft({ ...blankQuestion })
|
||||||
fetchExamInfo()
|
fetchExamInfo()
|
||||||
} else {
|
} else {
|
||||||
alert(`❌ ${data.error}`)
|
alert(`❌ ${data.error}`)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error('Upload error:', err)
|
||||||
alert('❌ Network error')
|
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 => ({
|
setDraft(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
test_cases: prev.test_cases.map((tc, i) =>
|
test_cases: prev.test_cases.map((tc, i) =>
|
||||||
i === index ? { ...tc, [field]: value } : tc
|
i === index ? { ...tc, [field]: value } : tc
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const removeTestCase = (index: number) => {
|
const removeTestCase = (index: number) => {
|
||||||
if (draft.test_cases.length <= 1) {
|
if (draft.test_cases.length <= 1) {
|
||||||
@@ -246,118 +330,92 @@ export default function EnhancedHostPanel() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Duration Update ---------- */
|
// Auto-distribute points when total points change
|
||||||
const updateDuration = async () => {
|
const redistributePoints = () => {
|
||||||
if (customDuration < 5) {
|
const pointsPerTest = Math.floor(draft.total_points / draft.test_cases.length)
|
||||||
alert('Minimum 5 minutes')
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!confirm('Start the exam now? Participants will be able to submit solutions.')) return
|
||||||
|
|
||||||
try {
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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()
|
const data = await res.json()
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert('✅ Duration updated')
|
alert('✅ Exam started successfully!')
|
||||||
setShowDurationEdit(false)
|
|
||||||
fetchExamInfo()
|
fetchExamInfo()
|
||||||
} else alert(`❌ ${data.error}`)
|
} else {
|
||||||
} catch {
|
alert(`❌ ${data.error}`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Start exam error:', err)
|
||||||
alert('❌ Network error')
|
alert('❌ Network error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Enhanced Question Upload Form ---------- */
|
const stopExam = async () => {
|
||||||
const EnhancedQuestionUploadForm = () => (
|
if (!confirm('Stop the exam immediately? This will end the exam for all participants.')) return
|
||||||
<div className="bg-gray-800 rounded-lg p-6">
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h3 className="text-lg font-bold flex items-center space-x-2">
|
|
||||||
<TestTube className="h-5 w-5 text-green-400" />
|
|
||||||
<span>📝 Create Question with Dynamic Scoring</span>
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowUploader(false)}
|
|
||||||
className="text-gray-400 hover:text-white"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Basic Question Info */}
|
try {
|
||||||
<div className="space-y-4 mb-6">
|
const res = await fetch('http://127.0.0.1:5000/api/exam/stop-exam', {
|
||||||
<input
|
method: 'POST',
|
||||||
type="text"
|
headers: { 'Content-Type': 'application/json' },
|
||||||
placeholder="Question Title"
|
body: JSON.stringify({ exam_code: examCode })
|
||||||
value={draft.title}
|
})
|
||||||
onChange={(e) => setDraft(prev => ({...prev, title: e.target.value}))}
|
const data = await res.json()
|
||||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<textarea
|
if (data.success) {
|
||||||
placeholder="Question Description"
|
alert('✅ Exam stopped successfully!')
|
||||||
value={draft.description}
|
fetchExamInfo()
|
||||||
onChange={(e) => setDraft(prev => ({...prev, description: e.target.value}))}
|
} else {
|
||||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 h-32"
|
alert(`❌ ${data.error}`)
|
||||||
/>
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Stop exam error:', err)
|
||||||
|
alert('❌ Network error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
/* ---------- FIXED Test Case Editor Component ---------- */
|
||||||
<select
|
const TestCaseEditor = React.memo(({
|
||||||
value={draft.difficulty}
|
testCase,
|
||||||
onChange={(e) => setDraft(prev => ({...prev, difficulty: e.target.value as any}))}
|
index,
|
||||||
className="p-3 bg-gray-700 rounded border border-gray-600"
|
onUpdate,
|
||||||
>
|
onRemove,
|
||||||
<option value="easy">Easy</option>
|
canRemove
|
||||||
<option value="medium">Medium</option>
|
}: {
|
||||||
<option value="hard">Hard</option>
|
testCase: TestCase
|
||||||
</select>
|
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])
|
||||||
|
|
||||||
<input
|
return (
|
||||||
type="number"
|
<div className="bg-gray-900 p-4 rounded mb-3 border border-gray-700">
|
||||||
value={draft.total_points}
|
|
||||||
onChange={(e) => setDraft(prev => ({...prev, total_points: parseInt(e.target.value) || 100}))}
|
|
||||||
className="p-3 bg-gray-700 rounded border border-gray-600"
|
|
||||||
placeholder="Total Points"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Host's Correct Solution */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<h4 className="font-medium mb-2 flex items-center space-x-2">
|
|
||||||
<Award className="h-4 w-4 text-gold-400" />
|
|
||||||
<span>✅ Your Correct Solution (Python):</span>
|
|
||||||
</h4>
|
|
||||||
<textarea
|
|
||||||
placeholder="Enter your correct solution here..."
|
|
||||||
value={draft.correct_solution.python}
|
|
||||||
onChange={(e) => setDraft(prev => ({
|
|
||||||
...prev,
|
|
||||||
correct_solution: {...prev.correct_solution, python: e.target.value}
|
|
||||||
}))}
|
|
||||||
className="w-full p-3 bg-gray-900 text-green-400 font-mono rounded border border-gray-600 h-32"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Test Cases */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h4 className="font-medium flex items-center space-x-2">
|
|
||||||
<TestTube className="h-4 w-4 text-blue-400" />
|
|
||||||
<span>🧪 Test Cases for Dynamic Scoring</span>
|
|
||||||
</h4>
|
|
||||||
<button
|
|
||||||
onClick={addTestCase}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded text-sm flex items-center space-x-1"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
<span>Add Test Case</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{draft.test_cases.map((testCase, index) => (
|
|
||||||
<div key={index} className="bg-gray-900 p-4 rounded mb-3 border border-gray-700">
|
|
||||||
<div className="flex justify-between items-center mb-3">
|
<div className="flex justify-between items-center mb-3">
|
||||||
<span className="font-medium text-blue-300">Test Case {index + 1}</span>
|
<span className="font-medium text-blue-300">Test Case {index + 1}</span>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
@@ -365,7 +423,7 @@ export default function EnhancedHostPanel() {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={testCase.is_public}
|
checked={testCase.is_public}
|
||||||
onChange={(e) => updateTestCase(index, 'is_public', e.target.checked)}
|
onChange={(e) => handleInputChange('is_public', e.target.checked)}
|
||||||
className="rounded"
|
className="rounded"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">Public</span>
|
<span className="text-sm">Public</span>
|
||||||
@@ -373,13 +431,16 @@ export default function EnhancedHostPanel() {
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={testCase.points}
|
value={testCase.points}
|
||||||
onChange={(e) => updateTestCase(index, 'points', parseInt(e.target.value) || 0)}
|
onChange={(e) => handleInputChange('points', parseInt(e.target.value) || 0)}
|
||||||
className="w-20 p-1 bg-gray-700 rounded text-sm border border-gray-600"
|
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"
|
placeholder="Points"
|
||||||
|
min="0"
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
{draft.test_cases.length > 1 && (
|
{canRemove && (
|
||||||
<button
|
<button
|
||||||
onClick={() => removeTestCase(index)}
|
type="button"
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
className="text-red-400 hover:text-red-300 text-sm"
|
className="text-red-400 hover:text-red-300 text-sm"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
@@ -393,10 +454,11 @@ export default function EnhancedHostPanel() {
|
|||||||
<label className="block text-sm mb-1">Input:</label>
|
<label className="block text-sm mb-1">Input:</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={testCase.input}
|
value={testCase.input}
|
||||||
onChange={(e) => updateTestCase(index, 'input', e.target.value)}
|
onChange={(e) => handleInputChange('input', e.target.value)}
|
||||||
className="w-full p-2 bg-gray-800 rounded text-sm border border-gray-600"
|
className="w-full p-2 bg-gray-800 rounded text-sm border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="Test input (leave empty if none)"
|
placeholder="Test input (leave empty if none)"
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -404,10 +466,11 @@ export default function EnhancedHostPanel() {
|
|||||||
<label className="block text-sm mb-1">Expected Output: <span className="text-red-400">*</span></label>
|
<label className="block text-sm mb-1">Expected Output: <span className="text-red-400">*</span></label>
|
||||||
<textarea
|
<textarea
|
||||||
value={testCase.expected_output}
|
value={testCase.expected_output}
|
||||||
onChange={(e) => updateTestCase(index, 'expected_output', e.target.value)}
|
onChange={(e) => handleInputChange('expected_output', e.target.value)}
|
||||||
className="w-full p-2 bg-gray-800 rounded text-sm border border-gray-600"
|
className="w-full p-2 bg-gray-800 rounded text-sm border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="Expected output (required)"
|
placeholder="Expected output (required)"
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,11 +478,163 @@ export default function EnhancedHostPanel() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={testCase.description}
|
value={testCase.description}
|
||||||
onChange={(e) => updateTestCase(index, 'description', e.target.value)}
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||||
className="w-full p-2 bg-gray-800 rounded text-sm border border-gray-600"
|
className="w-full p-2 bg-gray-800 rounded text-sm border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
placeholder="Test case description"
|
placeholder="Test case description"
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
/* ---------- FIXED Enhanced Question Upload Form ---------- */
|
||||||
|
const EnhancedQuestionUploadForm = () => (
|
||||||
|
<div className="bg-gray-800 rounded-lg p-6 mb-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h3 className="text-lg font-bold flex items-center space-x-2">
|
||||||
|
<TestTube className="h-5 w-5 text-green-400" />
|
||||||
|
<span>📝 Create Question with Dynamic Scoring</span>
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowUploader(false)}
|
||||||
|
className="text-gray-400 hover:text-white"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Question Info */}
|
||||||
|
<div className="space-y-4 mb-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Question Title (e.g., 'Print Hello World')"
|
||||||
|
value={draft.title}
|
||||||
|
onChange={handleTitleChange}
|
||||||
|
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
placeholder="Question Description (e.g., 'Write a program that prints Hello World')"
|
||||||
|
value={draft.description}
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
className="w-full p-3 bg-gray-700 rounded border border-gray-600 h-32 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<select
|
||||||
|
value={draft.difficulty}
|
||||||
|
onChange={handleDifficultyChange}
|
||||||
|
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
>
|
||||||
|
<option value="easy">Easy</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="hard">Hard</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={draft.total_points}
|
||||||
|
onChange={handleTotalPointsChange}
|
||||||
|
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
placeholder="Total Points"
|
||||||
|
min="1"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={redistributePoints}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 px-3 py-2 rounded text-sm"
|
||||||
|
title="Redistribute points evenly across test cases"
|
||||||
|
>
|
||||||
|
Redistribute Points
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Examples Section */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-medium mb-2 flex items-center space-x-2">
|
||||||
|
<Award className="h-4 w-4 text-blue-400" />
|
||||||
|
<span>📚 Examples (shown to participants):</span>
|
||||||
|
</h4>
|
||||||
|
{draft.examples.map((example, index) => (
|
||||||
|
<div key={index} className="bg-gray-900 p-3 rounded mb-2 border border-gray-700">
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Input"
|
||||||
|
value={example.input}
|
||||||
|
onChange={(e) => handleExampleChange(index, 'input', e.target.value)}
|
||||||
|
className="p-2 bg-gray-800 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Expected Output"
|
||||||
|
value={example.expected_output}
|
||||||
|
onChange={(e) => handleExampleChange(index, 'expected_output', e.target.value)}
|
||||||
|
className="p-2 bg-gray-800 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Description"
|
||||||
|
value={example.description}
|
||||||
|
onChange={(e) => handleExampleChange(index, 'description', e.target.value)}
|
||||||
|
className="w-full p-2 bg-gray-800 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Host's Correct Solution */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-medium mb-2 flex items-center space-x-2">
|
||||||
|
<Award className="h-4 w-4 text-gold-400" />
|
||||||
|
<span>✅ Your Correct Solution (Python):</span>
|
||||||
|
</h4>
|
||||||
|
<textarea
|
||||||
|
placeholder="Enter your correct solution here... (e.g., print('Hello World'))"
|
||||||
|
value={draft.correct_solution.python}
|
||||||
|
onChange={handleCorrectSolutionChange}
|
||||||
|
className="w-full p-3 bg-gray-900 text-green-400 font-mono rounded border border-gray-600 h-32 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test Cases */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h4 className="font-medium flex items-center space-x-2">
|
||||||
|
<TestTube className="h-4 w-4 text-blue-400" />
|
||||||
|
<span>🧪 Test Cases for Dynamic Scoring</span>
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addTestCase}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded text-sm flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
<span>Add Test Case</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{draft.test_cases.map((testCase, index) => (
|
||||||
|
<TestCaseEditor
|
||||||
|
key={index}
|
||||||
|
testCase={testCase}
|
||||||
|
index={index}
|
||||||
|
onUpdate={updateTestCase}
|
||||||
|
onRemove={removeTestCase}
|
||||||
|
canRemove={draft.test_cases.length > 1}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Test Case Summary */}
|
{/* Test Case Summary */}
|
||||||
@@ -435,6 +650,7 @@ export default function EnhancedHostPanel() {
|
|||||||
{/* Upload Button */}
|
{/* Upload Button */}
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={uploadQuestion}
|
onClick={uploadQuestion}
|
||||||
className="bg-green-600 hover:bg-green-700 px-6 py-2 rounded flex items-center space-x-2"
|
className="bg-green-600 hover:bg-green-700 px-6 py-2 rounded flex items-center space-x-2"
|
||||||
>
|
>
|
||||||
@@ -442,6 +658,7 @@ export default function EnhancedHostPanel() {
|
|||||||
<span>📤 Upload Enhanced Question</span>
|
<span>📤 Upload Enhanced Question</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setShowUploader(false)}
|
onClick={() => setShowUploader(false)}
|
||||||
className="bg-gray-600 hover:bg-gray-700 px-6 py-2 rounded"
|
className="bg-gray-600 hover:bg-gray-700 px-6 py-2 rounded"
|
||||||
>
|
>
|
||||||
@@ -452,18 +669,39 @@ export default function EnhancedHostPanel() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
/* ---------- Enhanced Participant Display ---------- */
|
/* ---------- Enhanced Participant Display ---------- */
|
||||||
const EnhancedParticipantsList = () => (
|
const EnhancedParticipantsList = () => {
|
||||||
|
const allParticipants = [...leaderboardData.leaderboard, ...leaderboardData.waiting_participants]
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{participants.map((participant, index) => (
|
{allParticipants.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
<Users className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>No participants yet</p>
|
||||||
|
<p className="text-sm">Share the exam code: <span className="font-bold text-blue-400">{examCode}</span></p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
allParticipants.map((participant, index) => (
|
||||||
<div key={index} className="bg-gray-800 p-4 rounded-lg border border-gray-700">
|
<div key={index} className="bg-gray-800 p-4 rounded-lg border border-gray-700">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium">{participant.name}</h4>
|
<h4 className="font-medium flex items-center space-x-2">
|
||||||
|
<span>{participant.name}</span>
|
||||||
|
{participant.completed && (
|
||||||
|
<span className="text-xs bg-green-600 px-2 py-1 rounded">Completed</span>
|
||||||
|
)}
|
||||||
|
{participant.rank && (
|
||||||
|
<span className="text-xs bg-blue-600 px-2 py-1 rounded">Rank #{participant.rank}</span>
|
||||||
|
)}
|
||||||
|
</h4>
|
||||||
<div className="text-sm text-gray-400 space-x-4">
|
<div className="text-sm text-gray-400 space-x-4">
|
||||||
<span>Joined: {new Date(participant.joined_at).toLocaleTimeString()}</span>
|
<span>Joined: {new Date(participant.joined_at).toLocaleTimeString()}</span>
|
||||||
{participant.completed && participant.submitted_at && (
|
{participant.completed && participant.submitted_at && (
|
||||||
<span>Submitted: {new Date(participant.submitted_at).toLocaleTimeString()}</span>
|
<span>Submitted: {new Date(participant.submitted_at).toLocaleTimeString()}</span>
|
||||||
)}
|
)}
|
||||||
|
{participant.language && (
|
||||||
|
<span>Language: {participant.language}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
@@ -485,47 +723,45 @@ export default function EnhancedHostPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rest of your existing component logic (exam lifecycle, UI, etc.)
|
|
||||||
const startExam = async () => {
|
|
||||||
if (!confirm('Start the exam now?')) return
|
|
||||||
try {
|
|
||||||
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 })
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
data.success ? fetchExamInfo() : alert(`❌ ${data.error}`)
|
|
||||||
} catch {
|
|
||||||
alert('❌ Network error')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopExam = async () => {
|
// Calculate time remaining for active exams
|
||||||
if (!confirm('Stop the exam immediately?')) return
|
const getTimeRemaining = () => {
|
||||||
try {
|
if (examInfo?.status !== 'active' || !examInfo.end_time) return null
|
||||||
const res = await fetch('http://127.0.0.1:5000/api/exam/stop-exam', {
|
|
||||||
method: 'POST',
|
const now = Date.now()
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const endTime = new Date(examInfo.end_time).getTime()
|
||||||
body: JSON.stringify({ exam_code: examCode })
|
const remaining = Math.max(0, Math.floor((endTime - now) / 1000))
|
||||||
})
|
|
||||||
const data = await res.json()
|
const minutes = Math.floor(remaining / 60)
|
||||||
data.success ? fetchExamInfo() : alert(`❌ ${data.error}`)
|
const seconds = remaining % 60
|
||||||
} catch {
|
|
||||||
alert('❌ Network error')
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Real-time timer for active exams
|
||||||
|
const [timeRemaining, setTimeRemaining] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (examInfo?.status === 'active' && examInfo.end_time) {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTimeRemaining(getTimeRemaining())
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearInterval(timer)
|
||||||
}
|
}
|
||||||
|
}, [examInfo])
|
||||||
|
|
||||||
/* =========================== RENDER =========================== */
|
/* =========================== RENDER =========================== */
|
||||||
if (loading) return (
|
if (loading) return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<RefreshCw className="h-8 w-8 animate-spin mb-4"/>
|
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-4"/>
|
||||||
<p>Loading enhanced host panel …</p>
|
<p>Loading enhanced host panel...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -533,12 +769,14 @@ export default function EnhancedHostPanel() {
|
|||||||
if (error || !examInfo) return (
|
if (error || !examInfo) return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<AlertCircle className="h-10 w-10 text-red-500 mb-4"/>
|
<AlertCircle className="h-10 w-10 text-red-500 mx-auto mb-4"/>
|
||||||
<p className="mb-2">{error || 'Unknown error'}</p>
|
<p className="mb-2">{error || 'Unknown error'}</p>
|
||||||
<button
|
<button
|
||||||
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
|
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
|
||||||
onClick={fetchExamInfo}
|
onClick={fetchExamInfo}
|
||||||
>Retry</button>
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -554,22 +792,37 @@ export default function EnhancedHostPanel() {
|
|||||||
<span>Enhanced Host Panel</span>
|
<span>Enhanced Host Panel</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-sm text-gray-400">
|
||||||
Exam Code: {examCode} • Dynamic Scoring Enabled
|
Exam Code: <span className="font-bold text-blue-400">{examCode}</span> • Dynamic Scoring Enabled
|
||||||
|
{timeRemaining && (
|
||||||
|
<span className="ml-4 text-orange-400">⏰ Time Remaining: {timeRemaining}</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
|
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
|
||||||
examInfo.status === 'waiting' ? 'bg-yellow-600' :
|
examInfo.status === 'waiting' ? 'bg-yellow-600' :
|
||||||
examInfo.status === 'active' ? 'bg-green-600' : 'bg-red-600'
|
examInfo.status === 'active' ? 'bg-green-600' : 'bg-red-600'
|
||||||
}`}>
|
}`}>
|
||||||
{examInfo.status.toUpperCase()}
|
{examInfo.status.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
fetchExamInfo()
|
||||||
|
fetchLeaderboard()
|
||||||
|
}}
|
||||||
|
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded"
|
||||||
|
title="Refresh"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Enhanced Tabs */}
|
{/* Enhanced Tabs */}
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
{[
|
{[
|
||||||
{ id: 'overview', label: 'Overview', icon: Trophy },
|
{ id: 'overview', label: 'Overview', icon: Trophy },
|
||||||
{ id: 'participants', label: 'Participants', icon: Users },
|
{ id: 'participants', label: `Participants (${leaderboardData.stats.total_participants})`, icon: Users },
|
||||||
{ id: 'questions', label: 'Questions', icon: TestTube }
|
{ id: 'questions', label: 'Questions', icon: TestTube }
|
||||||
].map(tab => (
|
].map(tab => (
|
||||||
<button
|
<button
|
||||||
@@ -603,27 +856,44 @@ export default function EnhancedHostPanel() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
<div className="bg-gray-900 p-3 rounded">
|
||||||
<div className="text-2xl font-bold text-blue-400">{participants.length}</div>
|
<div className="text-2xl font-bold text-blue-400">
|
||||||
|
{leaderboardData.stats.total_participants}
|
||||||
|
</div>
|
||||||
<div className="text-sm text-gray-400">Total Participants</div>
|
<div className="text-sm text-gray-400">Total Participants</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
<div className="bg-gray-900 p-3 rounded">
|
||||||
<div className="text-2xl font-bold text-green-400">
|
<div className="text-2xl font-bold text-green-400">
|
||||||
{participants.filter(p => p.completed).length}
|
{leaderboardData.stats.completed_submissions}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-400">Completed</div>
|
<div className="text-sm text-gray-400">Completed</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
<div className="bg-gray-900 p-3 rounded">
|
||||||
<div className="text-2xl font-bold text-purple-400">
|
<div className="text-2xl font-bold text-purple-400">
|
||||||
{Math.round(
|
{Math.round(leaderboardData.stats.average_score)}%
|
||||||
participants.filter(p => p.completed).reduce((sum, p) => sum + p.score, 0) /
|
|
||||||
Math.max(participants.filter(p => p.completed).length, 1)
|
|
||||||
)}%
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-400">Avg Score</div>
|
<div className="text-sm text-gray-400">Avg Score</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
<div className="bg-gray-900 p-3 rounded">
|
||||||
<div className="text-2xl font-bold text-orange-400">{examInfo.duration_minutes}m</div>
|
<div className="text-2xl font-bold text-orange-400">
|
||||||
<div className="text-sm text-gray-400">Duration</div>
|
{leaderboardData.stats.highest_score}%
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400">Highest Score</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Additional Stats */}
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-700">
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Duration:</span>
|
||||||
|
<span className="font-medium">{examInfo.duration_minutes}m</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Still Working:</span>
|
||||||
|
<span className="font-medium text-yellow-400">
|
||||||
|
{leaderboardData.stats.waiting_submissions}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -655,7 +925,7 @@ export default function EnhancedHostPanel() {
|
|||||||
className="w-full bg-yellow-600 hover:bg-yellow-700 p-3 rounded flex items-center justify-center space-x-2"
|
className="w-full bg-yellow-600 hover:bg-yellow-700 p-3 rounded flex items-center justify-center space-x-2"
|
||||||
>
|
>
|
||||||
<Timer className="h-4 w-4" />
|
<Timer className="h-4 w-4" />
|
||||||
<span>⏰ Edit Duration</span>
|
<span>⏰ Edit Duration ({examInfo.duration_minutes}m)</span>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -681,8 +951,13 @@ export default function EnhancedHostPanel() {
|
|||||||
min="5"
|
min="5"
|
||||||
max="180"
|
max="180"
|
||||||
/>
|
/>
|
||||||
|
<span className="flex items-center text-sm text-gray-400">minutes</span>
|
||||||
<button
|
<button
|
||||||
onClick={updateDuration}
|
onClick={() => {
|
||||||
|
// Update duration logic would go here
|
||||||
|
setShowDurationEdit(false)
|
||||||
|
alert('Duration update functionality needs backend endpoint')
|
||||||
|
}}
|
||||||
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
|
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
@@ -701,15 +976,21 @@ export default function EnhancedHostPanel() {
|
|||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h3 className="text-lg font-bold flex items-center space-x-2">
|
<h3 className="text-lg font-bold flex items-center space-x-2">
|
||||||
<Users className="h-5 w-5 text-blue-400" />
|
<Users className="h-5 w-5 text-blue-400" />
|
||||||
<span>Enhanced Participants ({participants.length})</span>
|
<span>Enhanced Participants ({leaderboardData.stats.total_participants})</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
Completed: {leaderboardData.stats.completed_submissions} |
|
||||||
|
Working: {leaderboardData.stats.waiting_submissions}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={fetchParticipants}
|
onClick={fetchLeaderboard}
|
||||||
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded"
|
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<EnhancedParticipantsList />
|
<EnhancedParticipantsList />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -734,17 +1015,53 @@ export default function EnhancedHostPanel() {
|
|||||||
|
|
||||||
{examInfo.problem_title ? (
|
{examInfo.problem_title ? (
|
||||||
<div className="bg-gray-900 p-4 rounded border border-green-600">
|
<div className="bg-gray-900 p-4 rounded border border-green-600">
|
||||||
<h4 className="font-medium text-green-400 mb-2">
|
<h4 className="font-medium text-green-400 mb-2 flex items-center space-x-2">
|
||||||
📝 {examInfo.problem_title}
|
<TestTube className="h-4 w-4" />
|
||||||
|
<span>📝 {examInfo.problem_title}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-gray-300 text-sm mb-3">
|
<p className="text-gray-300 text-sm mb-3">
|
||||||
{examInfo.problem_description || 'No description available'}
|
{examInfo.problem?.description || 'No description available'}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center space-x-4 text-sm text-gray-400">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
<span>✅ Dynamic scoring enabled</span>
|
<div className="flex items-center space-x-2 text-gray-400">
|
||||||
<span>🧪 Test case based</span>
|
<span>✅</span>
|
||||||
<span>🎯 Point distributed</span>
|
<span>Dynamic scoring enabled</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-gray-400">
|
||||||
|
<span>🧪</span>
|
||||||
|
<span>{examInfo.problem?.test_cases?.length || 0} test cases</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-gray-400">
|
||||||
|
<span>🎯</span>
|
||||||
|
<span>{examInfo.problem?.total_points || 100} total points</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-gray-400">
|
||||||
|
<span>📊</span>
|
||||||
|
<span>{examInfo.problem?.difficulty || 'medium'} difficulty</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test Cases Preview */}
|
||||||
|
{examInfo.problem?.test_cases && examInfo.problem.test_cases.length > 0 && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-700">
|
||||||
|
<h5 className="font-medium mb-2">Test Cases Preview:</h5>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{examInfo.problem.test_cases.slice(0, 3).map((tc, index) => (
|
||||||
|
<div key={index} className="bg-gray-800 p-2 rounded text-xs">
|
||||||
|
<span className="text-blue-400">Test {index + 1}:</span>
|
||||||
|
<span className="ml-2">{tc.expected_output || 'Hidden'}</span>
|
||||||
|
<span className="ml-2 text-green-400">(+{tc.points} pts)</span>
|
||||||
|
{tc.is_public && <span className="ml-2 text-yellow-400">[Public]</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{examInfo.problem.test_cases.length > 3 && (
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
...and {examInfo.problem.test_cases.length - 3} more test cases
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8 text-gray-400">
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
|||||||
Reference in New Issue
Block a user