/** * Fetch the answer data and clean it up based on the type of question * @param Array $potentialAnswers The potential answers that need checking. * @return Array A list of the results. */ function check_quizzes_canWeContinue_extractAnswerData($potentialAnswers) { $resultsList = array('answer_list' => array(), 'wrong_answer_list' => array(), 'error_answer_list' => array()); // #### 1 - Extract a list of actual answers from the potential answers. There will // be some noise in that data. Check the raw data to see what questions we have. foreach ($potentialAnswers as $key => $value) { // Only considering answers to questions. Format of answer field is: // question_16_truefalse_48 (first ID is quiz, 2nd ID is question, middle string // is the question type. if (preg_match('/^question_(\\d+)_([a-z]+)_(\\d+)$/', $key, $matches)) { $quizID = $matches[1]; $questionID = $matches[3]; $questionType = $matches[2]; // Again, check that answer matches quiz we're expecting. // Probably a little paranoid, but it's worth checking // to ensure there's nothing naughty going on. if ($quizID != $this->unitQuizDetails->quiz_id) { continue; } // Clean up the submitted data based on the type of quiz using the static checks in each // of the questions (to save loading whole class). If the data is valid, add the valid // answer to the list of fully validate danswers. switch ($questionType) { case 'multi': $resultsList['answer_list'][$questionID] = WPCW_quiz_MultipleChoice::sanitizeAnswerData($value); break; case 'truefalse': $resultsList['answer_list'][$questionID] = WPCW_quiz_TrueFalse::sanitizeAnswerData($value); break; case 'open': $resultsList['answer_list'][$questionID] = WPCW_quiz_OpenEntry::sanitizeAnswerData($value); break; // Ignore uploads as a $_POST field, simply because the files should be stored in $_FILES // not in $_POST. So if we have a file in $_FILES, that's clearly an issue. // Ignore uploads as a $_POST field, simply because the files should be stored in $_FILES // not in $_POST. So if we have a file in $_FILES, that's clearly an issue. case 'upload': break; } } // end of question check } // end of potential answers loop // ### 2 - Check for file uploads if the quiz requires them. Only check for uploads // if the quiz details say there should be some uploads. if ($this->unitQuizDetails->want_uploads) { $uploadResultList = WPCW_quiz_FileUpload::validateFiles($_FILES, $this->unitQuizDetails); // Merge the valid results. // Basically if a file has been uploaded correctly, that answer is marked as being set. if (count($uploadResultList['upload_valid']) > 0) { $resultsList['answer_list'] = $resultsList['answer_list'] + $uploadResultList['upload_valid']; } // Merge the error results if (count($uploadResultList['upload_errors']) > 0) { $resultsList['error_answer_list'] = $resultsList['error_answer_list'] + $uploadResultList['upload_errors']; } } return $resultsList; }
/** * Having entered some details into the quiz, may the user progress to the next unit? If * there are any problems with the quiz, then they are dealt with via AJAX. * * @param Object $quizDetails The potential quiz details. * @param Array $potentialAnswers The potential answers that need checking. * @param Integer $userID The ID of the user that we're saving progress for. * * @return Boolean True if the user may progress, false otherwise. */ function WPCW_quizzes_handleQuizRendering_canUserContinueAfterQuiz($quizDetails, $potentialAnswers, $userID) { $resultsList = array('answer_list' => array(), 'wrong_answer_list' => array(), 'error_answer_list' => array()); $resultDetails = array('correct' => array(), 'wrong' => array()); // #### 1A Extract a list of actual answers from the potential answers. There will // be some noise in that data. foreach ($potentialAnswers as $key => $value) { // Only considering answers to questions. Format of answer field is: // question_16_truefalse_48 (first ID is quiz, 2nd ID is question, middle string // is the question type. if (preg_match('/^question_(\\d+)_([a-z]+)_(\\d+)$/', $key, $matches)) { $quizID = $matches[1]; $questionID = $matches[3]; $questionType = $matches[2]; // Again, check that answer matches quiz we're expecting. // Probably a little paranoid, but it's worth checking // to ensure there's nothing naughty going on. if ($quizID != $quizDetails->quiz_id) { continue; } // Clean up the submitted data based on the type of quiz using the static checks in each // of the questions (to save loading whole class). If the data is valid, add the valid // answer to the list of fully validate danswers. switch ($questionType) { case 'multi': $resultsList['answer_list'][$questionID] = WPCW_quiz_MultipleChoice::sanitizeAnswerData($value); break; case 'truefalse': $resultsList['answer_list'][$questionID] = WPCW_quiz_TrueFalse::sanitizeAnswerData($value); break; case 'open': $resultsList['answer_list'][$questionID] = WPCW_quiz_OpenEntry::sanitizeAnswerData($value); break; // Ignore uploads as a $_POST field, simply because the files should be stored in $_FILES // not in $_POST. So if we have a file in $_FILES, that's clearly an issue. // Ignore uploads as a $_POST field, simply because the files should be stored in $_FILES // not in $_POST. So if we have a file in $_FILES, that's clearly an issue. case 'upload': break; } } // end of question check } // end of potential answers loop // ### 1B Check for file uploads if the quiz requires them. Only check for uploads // if the quiz details say there should be some uploads. if ($quizDetails->want_uploads) { $uploadResultList = WPCW_quiz_FileUpload::validateFiles($_FILES, $quizDetails); // Merge the valid results. // Basically if a file has been uploaded correctly, that answer is marked as being set. if (count($uploadResultList['upload_valid']) > 0) { $resultsList['answer_list'] = $resultsList['answer_list'] + $uploadResultList['upload_valid']; } // Merge the error results if (count($uploadResultList['upload_errors']) > 0) { $resultsList['error_answer_list'] = $resultsList['error_answer_list'] + $uploadResultList['upload_errors']; } } // ### 2 - Check that we have enough answers given how many questions there are. // If there are not enough answers, then re-render the form with the answered questions // marked, and highlight the fields that have errors. if ($quizDetails->questions && count($resultsList['answer_list']) < count($quizDetails->questions)) { // Error - not all questions are answered echo WPCW_units_createErrorMessage(__('Please provide an answer for all of the questions.', 'wp_courseware')); // Show the form with the questions that have been completed already. echo WPCW_quizzes_handleQuizRendering($quizDetails->parent_unit_id, $quizDetails, $resultsList); // User may not continue - as quiz is not complete. return false; } // Flag to indicate if grading is needed before the user continues. $gradingNeeded = false; $gradingNeededBeforeContinue = false; // ### 3 - Do we need to check for correct answers? if ('survey' == $quizDetails->quiz_type) { // Never try to show answers. There aren't any. $quizDetails->quiz_show_answers = 'hide_answers'; // No answers to check. Say thanks echo WPCW_units_createSuccessMessage(__('Thank you for your responses. This unit is now complete.', 'wp_courseware')); } else { $resultDetails = WPCW_quizzes_checkForCorrectAnswers($quizDetails, $resultsList['answer_list']); // #### Step A - have open-ended questions that need marking. if (!empty($resultDetails['needs_marking'])) { $gradingNeeded = true; $courseDetails = WPCW_courses_getCourseDetails($quizDetails->parent_course_id); // Non-blocking quiz - so allowed to continue, but will be graded later. if ('quiz_noblock' == $quizDetails->quiz_type) { echo WPCW_units_createSuccessMessage($courseDetails->course_message_quiz_open_grading_non_blocking); } else { echo WPCW_units_createSuccessMessage($courseDetails->course_message_quiz_open_grading_blocking); // Grading is needed before they continue, but don't want them to re-take the quiz. $gradingNeededBeforeContinue = true; } } else { // Copy over the wrong answers. $resultsList['wrong_answer_list'] = $resultDetails['wrong']; // Some statistics $correctCount = count($resultDetails['correct']); $totalQuestions = count($quizDetails->questions); $correctPercentage = number_format($correctCount / $totalQuestions * 100, 1); // Non-blocking quiz. if ('quiz_noblock' == $quizDetails->quiz_type) { // Store user quiz results echo WPCW_units_createSuccessMessage(sprintf(__('You got <b>%d out of %d (%d%%)</b> questions correct! This unit is now complete.', 'wp_courseware'), $correctCount, $totalQuestions, $correctPercentage)); // Notify the user of their grade. do_action('wpcw_quiz_graded', $userID, $quizDetails, $correctPercentage, false); } else { $minPassQuestions = $totalQuestions * ($quizDetails->quiz_pass_mark / 100); // They've passed. Report how many they got right. if ($correctPercentage >= $quizDetails->quiz_pass_mark) { echo WPCW_units_createSuccessMessage(sprintf(__('You got <b>%d out of %d (%d%%)</b> questions correct! This unit is now complete.', 'wp_courseware'), $correctCount, $totalQuestions, $correctPercentage)); // Notify the user of their grade. do_action('wpcw_quiz_graded', $userID, $quizDetails, $correctPercentage, false); } else { echo WPCW_units_createErrorMessage(sprintf(__('Unfortunately, you only got <b>%d out of %d (%d%%)</b> questions correct. You need to correctly answer <b>at least %d questions (%d%%)</b>.', 'wp_courseware'), $correctCount, $totalQuestions, $correctPercentage, $minPassQuestions, $quizDetails->quiz_pass_mark)); // Show form with error answers. echo WPCW_quizzes_handleQuizRendering($quizDetails->parent_unit_id, $quizDetails, $resultsList); // Errors, so the user cannot progress yet. return false; } } // end of blocking quiz check } } // end of survey check // ### 4 - Show the correct answers to the user? if ('show_answers' == $quizDetails->quiz_show_answers) { echo WPCW_quizzes_showAllCorrectAnswers($quizDetails); } // ### 5 - Save the user progress WPCW_quizzes_saveUserProgress($userID, $quizDetails, $resultDetails, $resultsList['answer_list']); // Questions need grading, notify the admin if ($gradingNeeded) { // Notify the admin that questions need answering. do_action('wpcw_quiz_needs_grading', $userID, $quizDetails); } // Questions need grading, so don't allow user to continue if ($gradingNeededBeforeContinue) { return false; } // If we get this far, the user may progress to next unit return true; }
/** * Function that allows a question to be edited. */ function WPCW_showPage_ModifyQuestion_load() { $page = new PageBuilder(true); $page->showPageHeader(__('Edit Single Question', 'wp_courseware'), '70%', WPCW_icon_getPageIconURL()); $questionID = false; // Check POST and GET if (isset($_GET['question_id'])) { $questionID = $_GET['question_id'] + 0; } else { if (isset($_POST['question_id'])) { $questionID = $_POST['question_id'] + 0; } } // Trying to edit a question $questionDetails = WPCW_questions_getQuestionDetails($questionID, true); // Abort if question not found. if (!$questionDetails) { $page->showMessage(__('Sorry, but that question could not be found.', 'wp_courseware'), true); $page->showPageFooter(); return; } // See if the question has been submitted for saving. if ('true' == WPCW_arrays_getValue($_POST, 'question_save_mode')) { WPCW_handler_questions_processSave(false, true); $page->showMessage(__('Question successfully updated.', 'wp_courseware')); // Assume save has happened, so reload the settings. $questionDetails = WPCW_questions_getQuestionDetails($questionID, true); } // Manually set the order to zero, as not needed for ordering in this context. $questionDetails->question_order = 0; switch ($questionDetails->question_type) { case 'multi': $quizObj = new WPCW_quiz_MultipleChoice($questionDetails); break; case 'truefalse': $quizObj = new WPCW_quiz_TrueFalse($questionDetails); break; case 'open': $quizObj = new WPCW_quiz_OpenEntry($questionDetails); break; case 'upload': $quizObj = new WPCW_quiz_FileUpload($questionDetails); break; default: die(__('Unknown quiz type: ', 'wp_courseware') . $questionDetails->question_type); break; } $quizObj->showErrors = true; $quizObj->needCorrectAnswers = true; $quizObj->hideDragActions = true; // #wpcw_quiz_details_questions = needed for media uploader // .wpcw_question_holder_static = needed for wrapping the question using existing HTML. printf('<div id="wpcw_quiz_details_questions"><ul class="wpcw_question_holder_static">'); // Create form wrapper, so that we can save this question. printf('<form method="POST" action="%s?page=WPCW_showPage_ModifyQuestion&question_id=%d" />', admin_url('admin.php'), $questionDetails->question_id); // Question hidden fields printf('<input name="question_id" type="hidden" value="%d" />', $questionDetails->question_id); printf('<input name="question_save_mode" type="hidden" value="true" />'); // Show the quiz so that it can be edited. We're re-using the code we have for editing questions, // to save creating any special form edit code. echo $quizObj->editForm_toString(); // Save and return buttons. printf('<div class="wpcw_button_group"><br/>'); printf('<a href="%s?page=WPCW_showPage_QuestionPool" class="button-secondary">%s</a> ', admin_url('admin.php'), __('« Return to Question Pool', 'wp_courseware')); printf('<input type="submit" class="button-primary" value="%s" />', __('Save Question Details', 'wp_courseware')); printf('</div>'); printf('</form>'); printf('</ul></div>'); $page->showPageFooter(); }
/** * Show the forms where the quiz answers can be editied. * * @param Integer $quizID the ID of the quiz to be edited. * @param Object $page The associated page object for showing messages. */ function WPCW_showPage_ModifyQuiz_showQuizEntryForms($quizID, $page) { // Handle the saving of quiz questions WPCW_showPage_ModifyQuiz_showQuizEntryForms_processSave($quizID, $page); global $wpdb, $wpcwdb; $wpdb->show_errors(); // Work out if we need correct answers or not. And what the pass mark is. $quizDetails = WPCW_quizzes_getQuizDetails($quizID, true); $needCorrectAnswers = 'survey' != $quizDetails->quiz_type; // Got URL in action to ensure we go to top of page to see the success message. printf('<form method="post" id="wpcw_question_update_form" action="%s&quiz_id=%d">', admin_url('admin.php?page=WPCW_showPage_ModifyQuiz'), $quizID); // Show the existing quiz questions as a series of forms. $quizItems = WPCW_quizzes_getListOfQuestions($quizID); // Show the number of correct answers the user must get in order to pass. if ('quiz_block' == $quizDetails->quiz_type) { $totalQs = count($quizItems); $passQs = ceil($quizDetails->quiz_pass_mark / 100 * $totalQs); printf('<div class="wpcw_msg wpcw_msg_info">'); printf(__('The trainee will be required to correctly answer at least <b>%d of the %d</b> following questions (<b>at least %d%%</b>) to progress.', 'wp_courseware'), $passQs, $totalQs, $quizDetails->quiz_pass_mark); printf('</div>'); } // Got a quiz, and trainer is requiring to show answers. Tell them we can't show answers // as this quiz contains open-ended questions that need grading. if ($needCorrectAnswers && 'show_answers' == $quizDetails->quiz_show_answers && WPCW_quizzes_containsQuestionsNeedingManualGrading($quizItems)) { printf('<div class="wpcw_msg wpcw_msg_error">'); printf(__('This quiz contains questions that need <b>manual grading</b>, and you\'ve selected \'<b>Show Answers</b>\' when the user completes this quiz. ', 'wp_courseware') . '<br/><br/>' . __('Since answers cannot be shown to the user because they are not known at that stage, <b>answers cannot be shown</b>. To hide this message, select \'<b>No Answers</b>\' above.')); printf('</div>'); } $errorCount = 0; global $errorCount; // Wrapper for questions printf('<ol class="wpcw_dragable_question_holder">'); if ($quizItems) { // Render edit form for each of the quizzes that already exist foreach ($quizItems as $quizItem) { switch ($quizItem->question_type) { case 'multi': $quizObj = new WPCW_quiz_MultipleChoice($quizItem); break; case 'truefalse': $quizObj = new WPCW_quiz_TrueFalse($quizItem); break; case 'open': $quizObj = new WPCW_quiz_OpenEntry($quizItem); break; case 'upload': $quizObj = new WPCW_quiz_FileUpload($quizItem); break; default: die(__('Unknown quiz type: ', 'wp_courseware') . $quizItem->question_type); break; } $quizObj->showErrors = true; $quizObj->needCorrectAnswers = $needCorrectAnswers; // Keep track of errors if ($quizObj && $quizObj->gotError) { $errorCount++; } echo $quizObj->editForm_toString(); } } printf('</ol>'); // Do any of the questions have residual errors? Tell the user. if ($errorCount > 0) { $page->showMessage(sprintf(__('%d of the questions below have errors. Please make corrections and then save the changes.', 'wp_courseware'), $errorCount), true); } $page->showPageMiddle('35%'); // Show the menu for saving and adding new items. WPCW_showPage_ModifyQuiz_FloatMenu($page); // Flag to indicate that questions have been updated. printf('<input type="hidden" name="survey_updated" value="survey_updated" />'); printf('</form><a name="new_question"></a>'); // The empty forms for adding a new question $quizItemDummy = new stdClass(); $quizItemDummy->question_question = ''; $quizItemDummy->question_answers = "\n\n\n"; $quizItemDummy->question_correct_answer = false; $quizItemDummy->question_order = 0; $quizItemDummy->question_answer_type = false; $quizItemDummy->question_answer_hint = false; $quizItemDummy->question_answer_file_types = 'doc, pdf, jpg, png, jpeg, gif'; $quizFormsToCreate = array('new_multi' => 'WPCW_quiz_MultipleChoice', 'new_tf' => 'WPCW_quiz_TrueFalse', 'new_open' => 'WPCW_quiz_OpenEntry', 'new_upload' => 'WPCW_quiz_FileUpload'); // Create the dummy quiz objects foreach ($quizFormsToCreate as $dummyid => $objClass) { // Set placeholder class $quizItemDummy->question_id = $dummyid; // Create new object and set it up with defaults $quizObj = new $objClass($quizItemDummy); $quizObj->cssClasses = 'wpcw_question_template'; $quizObj->showErrors = false; $quizObj->needCorrectAnswers = $needCorrectAnswers; echo $quizObj->editForm_toString(); } }