Take Exam
Take Exam
import [Link];
import [Link].*;
import [Link].*;
import [Link];
import [Link];
import [Link].*;
import [Link].*;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
[Link] = getStudentName(studentId);
if ([Link] == null) {
[Link] = "Unknown Student";
[Link](this, "Could not retrieve student name.
Please contact support.", "Error", JOptionPane.ERROR_MESSAGE);
}
[Link](examInfoPanel, [Link]);
add(topPanel, [Link]);
// Initial states
disableAllExamControls();
[Link](false);
[Link](false);
[Link](false); // Hide until an exam is finished or
determined it's a retake candidate
[Link](prevButton);
[Link](nextButton);
[Link](finishCurrentExamButton);
[Link](submitAllExamsButton);
add(bottomPanel, [Link]);
/**
* Initializes the exam session by loading details for all available exams
* and then displaying the first one.
*/
private void initializeExamSession() {
if (examIdsInSession == null || [Link]()) {
[Link](this, "No exams provided for this
session. Please contact support.", "Session Error", JOptionPane.ERROR_MESSAGE);
[Link]("No exams to take.");
disableAllExamControls();
redirectToDashboard(); // Go back to dashboard if no exams
return;
}
/**
* Loads and displays the exam at the specified index in the session.
* Manages transitioning between exams or ending the session.
*/
private void loadAndDisplayExamAtIndex(int index) {
stopExamTimer(); // Stop timer for previous exam/transition
currentExamIndexInSession = index;
int examId = [Link](currentExamIndexInSession);
if ([Link]() == -1) {
[Link](this, "Failed to start/resume attempt for
exam '" + [Link]() + "'. Please try again or contact
support.", "Error", JOptionPane.ERROR_MESSAGE);
disableAllExamControls();
redirectToDashboard(); // Critical error, go back to dashboard
return;
}
if ([Link]().isEmpty()) {
[Link](this, "No questions available for '" +
[Link]() + "'. Skipping this exam and moving to next.",
"No Questions", JOptionPane.WARNING_MESSAGE);
disableExamControlsForCurrentExam();
loadAndDisplayExamAtIndex(currentExamIndexInSession + 1); // Skip to
next
return;
}
/**
* Helper to get exam title by ID, primarily for messages when details might
not be fully loaded.
*/
private String getExamTitleById(int examId) {
String title = "Unknown Exam";
Connection con = null;
try {
con = [Link]();
String query = "SELECT title FROM exams WHERE exam_id = ?";
PreparedStatement pst = [Link](query);
[Link](1, examId);
ResultSet rs = [Link]();
if ([Link]()) {
title = [Link]("title");
}
} catch (SQLException e) {
[Link]("Error getting exam title by ID: " +
[Link]());
} finally {
if (con != null) {
try {
[Link]();
} catch (SQLException e) {
[Link]();
}
}
}
return title;
}
/**
* Loads exam details (title, duration, question limit, shuffle, is_allowed)
for the given ExamSessionData object.
*/
private void loadExamDetailsForCurrentSessionData(ExamSessionData sessionData)
{
Connection con = null;
try {
con = [Link]();
String query = """
SELECT [Link], [Link], COALESCE(sea.question_limit, 0) AS
question_limit, COALESCE(sea.shuffle_questions, 0) AS shuffle_questions,
COALESCE(sea.is_allowed, FALSE) AS is_allowed
FROM exams e
LEFT JOIN student_exam_access sea ON e.exam_id = sea.exam_id AND
sea.student_id = ?
WHERE e.exam_id = ?
"""; // Changed JOIN to LEFT JOIN to handle cases where
student_exam_access entry might not exist yet
PreparedStatement pst = [Link](query);
[Link](1, studentId);
[Link](2, [Link]());
ResultSet rs = [Link]();
if ([Link]()) {
[Link]([Link]("title"));
[Link]([Link]("duration"));
[Link]([Link]("question_limit"));
[Link]([Link]("shuffle_questions"));
[Link]([Link]("is_allowed"));
} else {
[Link]("Unknown Exam (ID: " + [Link]()
+ ")");
[Link](false); // Default to not allowed if no
access record
[Link](this, "Could not retrieve access
details for exam ID: " + [Link](), "Error",
JOptionPane.ERROR_MESSAGE);
}
} catch (SQLException e) {
[Link]();
[Link](this, "Database error loading exam
details: " + [Link](), "DB Error", JOptionPane.ERROR_MESSAGE);
[Link](false);
} finally {
if (con != null) {
try {
[Link]();
} catch (SQLException e) {
[Link]();
}
}
}
}
/**
* Retrieves an existing attempt ID or creates a new one for the current
student and exam.
* Marks the attempt as started.
*/
private int getOrCreateAttemptId(int studentId, int examId) {
int attemptId = -1;
Connection con = null;
try {
con = [Link]();
// Check for existing uncompleted attempt
String checkAttemptQuery = "SELECT ex_id FROM exam_attempts WHERE
student_id = ? AND exam_id = ? AND is_completed = 0 ORDER BY start_time DESC LIMIT
1";
PreparedStatement checkPst = [Link](checkAttemptQuery);
[Link](1, studentId);
[Link](2, examId);
ResultSet rs = [Link]();
if ([Link]()) {
attemptId = [Link]("ex_id");
// Update start_time to now (resuming)
String updateStartTimeQuery = "UPDATE exam_attempts SET start_time
= NOW(), last_activity = NOW() WHERE ex_id = ?";
PreparedStatement updatePst =
[Link](updateStartTimeQuery);
[Link](1, attemptId);
[Link]();
[Link]("Resuming attempt: " + attemptId + " for exam: "
+ examId);
} else {
// No uncompleted attempt found, create a new one
String insertAttemptQuery = "INSERT INTO exam_attempts (student_id,
exam_id, start_time, last_activity, is_completed, score) VALUES (?, ?, NOW(),
NOW(), 0, 0)";
PreparedStatement insertPst =
[Link](insertAttemptQuery, Statement.RETURN_GENERATED_KEYS);
[Link](1, studentId);
[Link](2, examId);
[Link]();
/**
* Loads questions for the current exam. Shuffles if specified.
*/
private void loadQuestionsForCurrentExam(ExamSessionData sessionData) {
List<Question> questions = new ArrayList<>();
String query = "SELECT question_id, question_text, option_a, option_b,
option_c, option_d, correct_answer, marks " +
"FROM questions " +
"WHERE exam_id = ?";
Connection con = null;
try {
con = [Link]();
PreparedStatement pst = [Link](query);
[Link](1, [Link]());
ResultSet rs = [Link]();
while ([Link]()) {
[Link](new Question(
[Link]("question_id"),
[Link]("question_text"),
[Link]("option_a"),
[Link]("option_b"),
[Link]("option_c"),
[Link]("option_d"),
[Link]("correct_answer"),
[Link]("marks")
));
}
if ([Link]()) {
[Link](questions);
}
} catch (SQLException e) {
[Link]();
[Link](this, "Database error loading questions:
" + [Link](), "DB Error", JOptionPane.ERROR_MESSAGE);
} finally {
if (con != null) {
try {
[Link]();
} catch (SQLException e) {
[Link]();
}
}
}
}
/**
* Loads previously saved answers for a given attempt ID and populates the map.
*/
private void loadSavedAnswersForAttempt(int attemptId, Map<Integer, String>
savedAnswers) {
String query = "SELECT question_id, selected_option FROM student_answers
WHERE attempt_id = ?";
Connection con = null;
try {
con = [Link]();
PreparedStatement pst = [Link](query);
[Link](1, attemptId);
ResultSet rs = [Link]();
while ([Link]()) {
[Link]([Link]("question_id"),
[Link]("selected_option"));
}
} catch (SQLException e) {
[Link]();
[Link]("Error loading saved answers: " + [Link]());
} finally {
if (con != null) {
try {
[Link]();
} catch (SQLException e) {
[Link]();
}
}
}
}
/**
* Displays the question at the specified index.
*/
private void showQuestion(int index) {
if (currentExamSessionData == null ||
[Link]().isEmpty() || index < 0 || index >=
[Link]().size()) {
resetQuestionDisplay();
return;
}
currentQuestionIndexInExam = index;
Question currentQuestion =
[Link]().get(currentQuestionIndexInExam);
/**
* Saves the selected answer to the in-memory map and also to the database.
* Updates `last_activity` in `exam_attempts` to keep the session alive.
*/
private void saveAnswer(int questionId, String selectedOption) {
// Save to in-memory map
Map<Integer, String> examAnswers =
[Link]([Link](), new
HashMap<>());
[Link](questionId, selectedOption);
[Link]([Link](), examAnswers);
// Save to database
Connection con = null;
try {
con = [Link]();
String query = "INSERT INTO student_answers (attempt_id, question_id,
selected_option) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE selected_option = ?"; // UPSERT:
Insert if not exists, update if exists
PreparedStatement pst = [Link](query);
[Link](1, [Link]());
[Link](2, questionId);
[Link](3, selectedOption);
[Link](4, selectedOption); // For ON DUPLICATE KEY UPDATE
[Link]();
} catch (SQLException e) {
[Link]();
[Link]("Error saving answer to database: " +
[Link]());
} finally {
if (con != null) {
try {
[Link]();
} catch (SQLException e) {
[Link]();
}
}
}
}
/**
* Saves the currently selected answer for the displayed question.
* Called before moving to next/previous question or finishing exam.
*/
private void saveCurrentQuestionAnswer() {
ButtonModel selected = [Link]();
if (selected != null && currentExamSessionData != null && !
[Link]().isEmpty()) {
int currentQuestionId =
[Link]().get(currentQuestionIndexInExam).getQuestionId
();
saveAnswer(currentQuestionId, [Link]());
}
}
/**
* Updates the enabled state of navigation buttons.
*/
private void updateNavigationButtons() {
if (currentExamSessionData != null && !
[Link]().isEmpty()) {
[Link](currentQuestionIndexInExam > 0);
[Link](currentQuestionIndexInExam <
[Link]().size() - 1);
[Link](!
[Link]()); // Only enable if not already finished
} else {
// Disable all if no exam or no questions
[Link](false);
[Link](false);
[Link](false);
}
}
/**
* Disables all exam-related controls and resets the display.
*/
private void disableAllExamControls() {
[Link]("Exam Not Available / Session Ended.");
[Link]();
for (JRadioButton button : optionButtons) {
[Link]("");
[Link](false);
}
[Link](false);
[Link](false);
[Link](false);
[Link](false); // Also disable "Submit All"
stopExamTimer();
[Link]("Time: --:--");
/**
* Disables controls for the current exam, but doesn't necessarily end the
session.
* Useful when an exam has no questions or access issues.
*/
private void disableExamControlsForCurrentExam() {
[Link]("No questions or exam not accessible.");
[Link]();
for (JRadioButton button : optionButtons) {
[Link]("");
[Link](false);
}
[Link](false);
[Link](false);
[Link](false);
stopExamTimer();
[Link]("Time: --:--");
[Link](false); // Disable for this specific exam
[Link]("");
}
/**
* Enables exam controls (radio buttons, navigation).
*/
private void enableExamControls() {
for (JRadioButton button : optionButtons) {
[Link](true);
}
updateNavigationButtons();
}
/**
* Resets the question display area.
*/
private void resetQuestionDisplay() {
[Link]("No exam questions to display.");
[Link]();
for (JRadioButton button : optionButtons) {
[Link]("");
[Link](false);
}
}
/**
* Manages finishing the currently active exam.
*/
private void finishCurrentExam() {
if (currentExamSessionData == null || [Link]())
{
[Link](this, "This exam has already been
finished or is not active.", "Info", JOptionPane.INFORMATION_MESSAGE);
return;
}
if (confirm == JOptionPane.YES_OPTION) {
saveCurrentQuestionAnswer(); // Save the last answer
stopExamTimer(); // Stop the timer for this exam
calculateAndSaveAttemptScore(currentExamSessionData);
[Link](); // Mark as completed in memory
// Move to the next exam in the session, or end session and redirect
// loadAndDisplayExamAtIndex will handle the final redirect to
dashboard if no more exams
loadAndDisplayExamAtIndex(currentExamIndexInSession + 1);
}
}
/**
* Calculates the score for a given exam attempt and saves it to the database.
* Also updates `is_completed` and `end_time` in `exam_attempts`.
* Increments `attempts_taken` in `student_exam_access` (or updates it based on
count).
*/
private void calculateAndSaveAttemptScore(ExamSessionData sessionData) {
int score = 0;
int totalPossibleMarks = 0;
Map<Integer, String> studentAnswers =
[Link]([Link](), new HashMap<>());
[Link](1, studentId);
[Link](2, [Link]());
[Link](3, [Link]());
[Link](4, [Link]());
[Link](5, studentSelectedOption != null ?
studentSelectedOption : "N/A");
[Link](6, [Link]());
[Link](7, marksAwarded);
[Link](8, [Link]());
[Link](); // Add to batch for efficiency
}
} catch (SQLException e) {
[Link]();
try {
if (con != null) [Link](); // Rollback on error
} catch (SQLException ex) {
[Link]("Rollback failed: " + [Link]());
}
[Link](this, "Database error saving score or
updating attempt: " + [Link](), "DB Error", JOptionPane.ERROR_MESSAGE);
} finally {
if (con != null) {
try {
[Link](true); // Re-enable auto-commit
[Link]();
} catch (SQLException e) {
[Link]();
}
}
}
}
/**
* Checks if an exam is already marked as completed in the database for the
given student.
*/
private boolean isExamCompletedInDB(int studentId, int examId) {
String query = "SELECT COUNT(*) FROM exam_attempts WHERE student_id = ? AND
exam_id = ? AND is_completed = 1";
Connection con = null;
try {
con = [Link]();
PreparedStatement pst = [Link](query);
[Link](1, studentId);
[Link](2, examId);
ResultSet rs = [Link]();
if ([Link]()) {
return [Link](1) > 0;
}
} catch (SQLException e) {
[Link]();
[Link]("Error checking exam completion status in DB: " +
[Link]());
} finally {
if (con != null) {
try {
[Link]();
} catch (SQLException e) {
[Link]();
}
}
}
return false;
}
/**
* Starts the exam timer.
* @param durationMinutes The duration of the exam in minutes.
*/
private void startExamTimer(int durationMinutes) {
stopExamTimer(); // Ensure any previous timer is stopped
/**
* Stops the exam timer.
*/
private void stopExamTimer() {
if (timer != null) {
[Link]();
[Link]();
timer = null;
}
timerRunning = false;
}
/**
* Handles what happens when the timer runs out.
*/
private void timerExpired() {
stopExamTimer();
[Link](this, "Time's up for '" +
[Link]() + "'! Your current exam will be submitted.",
"Time Expired", JOptionPane.WARNING_MESSAGE);
finishCurrentExam(); // Automatically finish the current exam
}
/**
* Submits all remaining exams in the session that haven't been completed yet.
*/
private void submitAllRemainingExams() {
int confirm = [Link](this,
"Are you sure you want to submit all remaining exams in this
session?\n" +
"Any unattempted or unfinished exams will be marked as
submitted with current progress.",
"Confirm Submit All Exams", JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (confirm == JOptionPane.YES_OPTION) {
// First, finish the currently active exam if any
if (currentExamSessionData != null && !
[Link]()) {
saveCurrentQuestionAnswer(); // Save the last answer
stopExamTimer();
calculateAndSaveAttemptScore(currentExamSessionData);
[Link]();
}
// Iterate through the remaining exams in the session and mark them as
completed
// This is crucial for exams that were skipped or not fully attempted
for (int i = 0; i < [Link](); i++) {
int examIdToFinish = [Link](i);
// Only process if it's NOT the current exam (which was handled
above)
// AND it's not already completed in the DB.
if (examIdToFinish != [Link]() ||
[Link]()) {
if (!isExamCompletedInDB(studentId, examIdToFinish)) {
// Create a dummy session data for uncompleted exams to
save their state
ExamSessionData dummySessionData = new
ExamSessionData(examIdToFinish);
loadExamDetailsForCurrentSessionData(dummySessionData); //
Load details to get title/duration
[Link](getOrCreateAttemptId(studentId, examIdToFinish)); //
Get or create attempt
// No need to load questions if no answers were selected,
as score will be 0.
// However, if you want totalPossibleMarks to be accurate
for unattempted exams,
// you would need to load questions here. For now, it's
fine for score 0.
if (![Link]().isEmpty()) { // Ensure
questions are loaded for total marks
loadQuestionsForCurrentExam(dummySessionData);
}
calculateAndSaveAttemptScore(dummySessionData); // Mark as
completed with current (likely 0) score
[Link]();
[Link]("Force-submitted exam: " +
[Link]() + " due to 'Submit All'.");
}
}
}
[Link](this, "All exams in this session have
been submitted.", "Session Concluded", JOptionPane.INFORMATION_MESSAGE);
redirectToDashboard(); // Always redirect to dashboard after submitting
all
}
}
/**
* Handles requesting a retake for the current exam.
* This method is only visible if the exam is already completed.
*/
private void requestRetake() {
if (currentExamSessionData == null) {
[Link](this, "No active exam to request retake
for.", "Error", JOptionPane.WARNING_MESSAGE);
return;
}
if (confirm == JOptionPane.YES_OPTION) {
Connection con = null;
try {
con = [Link]();
// Check for existing pending request
String checkPendingQuery = "SELECT COUNT(*) FROM
exam_retake_requests WHERE student_id = ? AND exam_id = ? AND status = 'Pending'";
PreparedStatement checkPst =
[Link](checkPendingQuery);
[Link](1, studentId);
[Link](2, examId);
ResultSet rs = [Link]();
if ([Link]() && [Link](1) > 0) {
[Link](this, "You already have a pending
retake request for '" + examTitle + "'. Please wait for examiner's decision.",
"Request Already Pending", JOptionPane.INFORMATION_MESSAGE);
return;
}
/**
* Updates the visibility and text of the retake request button and status
label.
*/
private void updateRetakeRequestButtonStatus() {
if (currentExamSessionData == null) {
[Link](false);
[Link](false);
[Link]("");
return;
}
int examId = [Link]();
boolean examCompleted = isExamCompletedInDB(studentId, examId);
if (examCompleted) {
[Link](true);
Connection con = null;
try {
con = [Link]();
String query = "SELECT status FROM exam_retake_requests WHERE
student_id = ? AND exam_id = ? ORDER BY request_date DESC LIMIT 1";
PreparedStatement pst = [Link](query);
[Link](1, studentId);
[Link](2, examId);
ResultSet rs = [Link]();
if ([Link]()) {
String status = [Link]("status");
[Link]("Retake Status: " + status);
switch (status) {
case "Pending":
[Link](false); // Can't request
if already pending
break;
case "Granted":
[Link](false); // Already
granted, student should be able to take it from dashboard (auto-start)
[Link]("Retake Status:
Approved!"); // More user-friendly
break;
case "Denied":
[Link](true); // Can request
again
break;
default:
[Link](true); // Fallback to
allow request
break;
}
} else {
[Link](""); // No previous request
[Link](true); // Allow requesting
retake
}
} catch (SQLException e) {
[Link]();
[Link]("Error checking retake status.");
[Link](false);
} finally {
if (con != null) {
try {
[Link]();
} catch (SQLException e) {
[Link]();
}
}
}
} else {
// Exam not completed, retake not applicable
[Link](false);
[Link](false);
[Link]("");
}
}
/**
* Redirects the student back to the main dashboard.
*/
private void redirectToDashboard() {
stopExamTimer(); // Ensure timer is stopped
disableAllExamControls(); // Clean up UI state
if (parentDashboard != null) {
// Restore default close operation.
// [Link](JFrame.EXIT_ON_CLOSE); //
This is handled by showSelectExamPanel
/**
* Represents a single question in an exam.
*/
private static class Question {
private int questionId;
private String questionText;
private String optionA, optionB, optionC, optionD;
private String correctAnswer;
private int marks;
// Getters
public int getQuestionId() { return questionId; }
public String getQuestionText() { return questionText; }
public String getOptionA() { return optionA; }
public String getOptionB() { return optionB; }
public String getOptionC() { return optionC; }
public String getOptionD() { return optionD; }
public String getCorrectAnswer() { return correctAnswer; }
public int getMarks() { return marks; }
}
/**
* Holds all data pertinent to the current exam being taken in the session.
*/
private static class ExamSessionData {
private int examId;
private String title;
private int duration; // in minutes
private int questionLimit;
private boolean shuffleQuestions;
private boolean isAllowed; // From student_exam_access
private List<Question> questions;
private int attemptId;
private boolean completed;
private int score;
private int totalPossibleMarks;