/**
  * 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;
 }
Example #2
0
/**
 * Handle saving questions to the database.
 * @param Integer $quizID The quiz for which the questions apply to.
 * @param Object $page The associated page object for showing messages. 
 */
function WPCW_showPage_ModifyQuiz_showQuizEntryForms_processSave($quizID, $page)
{
    // No updates have been requested, so exit.
    if (!isset($_POST['survey_updated'])) {
        return;
    }
    global $wpdb, $wpcwdb;
    $wpdb->show_errors();
    $questionsToSave = array();
    $questionsToSave_New = array();
    // Check $_POST data for the
    foreach ($_POST as $key => $value) {
        // #### 1 - Check if we're deleting a question?
        // Not interested in new questions that have been added and then deleted. Just the
        // ones that were added to the database first.
        if (preg_match('/^delete_wpcw_quiz_details_([0-9]+)$/', $key, $matches)) {
            $SQL = $wpdb->prepare("\n\t\t\t\tDELETE FROM {$wpcwdb->quiz_qs}\n\t\t\t\tWHERE question_id = %d\n\t\t\t", $matches[1]);
            $wpdb->query($SQL);
        } else {
            if (preg_match('/^question_question_(new_question_)?([0-9]+)$/', $key, $matches)) {
                // Got the ID of the question, now get answers and correct answer.
                $questionID = $matches[2];
                // Store the extra string if we're adding a new question.
                $newQuestionPrefix = $matches[1];
                $fieldName_Answers = 'question_answer_' . $newQuestionPrefix . $questionID;
                $fieldName_Correct = 'question_answer_sel_' . $newQuestionPrefix . $questionID;
                $fieldName_Type = 'question_type_' . $newQuestionPrefix . $questionID;
                $fieldName_Order = 'question_order_' . $newQuestionPrefix . $questionID;
                $fieldName_AnswerType = 'question_answer_type_' . $newQuestionPrefix . $questionID;
                $fieldName_AnswerHint = 'question_answer_hint_' . $newQuestionPrefix . $questionID;
                // Order should be a number
                $questionOrder = 0;
                if (isset($_POST[$fieldName_Order])) {
                    $questionOrder = $_POST[$fieldName_Order] + 0;
                }
                // Default types
                $qAns = false;
                $qAnsCor = false;
                $qAnsType = false;
                // Just used for open question types.
                $qAnsFileTypes = false;
                // Just used for upload file types.
                // Get the hint - Just used for open and upload types. Allow HTML.
                $qAnsHint = trim(WPCW_arrays_getValue($_POST, $fieldName_AnswerHint));
                // What type of question do we have?
                $questionType = WPCW_arrays_getValue($_POST, $fieldName_Type);
                switch ($questionType) {
                    case 'multi':
                        $qAns = WPCW_quiz_MultipleChoice::editSave_extractAnswerList($fieldName_Answers);
                        $qAnsCor = WPCW_quiz_MultipleChoice::editSave_extractCorrectAnswer($qAns, $fieldName_Correct);
                        // Provide the UI with at least once slot for an answer.
                        if (!$qAns) {
                            $qAns = array('', '');
                        }
                        break;
                    case 'open':
                        // See if there's a question type that's been sent back to the server.
                        $answerTypes = WPCW_quiz_OpenEntry::getValidAnswerTypes();
                        $thisAnswerType = WPCW_arrays_getValue($_POST, $fieldName_AnswerType);
                        // Validate the answer type is in the list. Don't create a default so that user must choose.
                        if (isset($answerTypes[$thisAnswerType])) {
                            $qAnsType = $thisAnswerType;
                        }
                        // There's no correct answer for an open question.
                        $qAnsCor = false;
                        break;
                    case 'upload':
                        $fieldName_FileType = 'question_answer_file_types_' . $newQuestionPrefix . $questionID;
                        // Check new file extension types, parsing them.
                        $qAnsFileTypesRaw = WPCW_files_cleanFileExtensionList(WPCW_arrays_getValue($_POST, $fieldName_FileType));
                        $qAnsFileTypes = implode(',', $qAnsFileTypesRaw);
                        break;
                    case 'truefalse':
                        $qAnsCor = WPCW_quiz_TrueFalse::editSave_extractCorrectAnswer($fieldName_Correct);
                        break;
                        // Not expecting anything here... so not handling the error case.
                    // Not expecting anything here... so not handling the error case.
                    default:
                        break;
                }
                // ### 4 - Save new question data as a list ready for saving to the database.
                // New question - so no question ID as yet
                if ($newQuestionPrefix) {
                    $questionsToSave_New[] = array('question_question' => stripslashes($value), 'question_answers' => $qAns ? implode("\n", $qAns) : $qAns, 'question_correct_answer' => $qAnsCor, 'parent_quiz_id' => $quizID, 'question_type' => $questionType, 'question_order' => $questionOrder, 'question_answer_type' => $qAnsType, 'question_answer_hint' => stripslashes($qAnsHint), 'question_answer_file_types' => $qAnsFileTypes);
                } else {
                    $questionsToSave[$questionID] = array('question_id' => $questionID, 'question_question' => stripslashes($value), 'question_answers' => $qAns ? implode("\n", $qAns) : $qAns, 'question_correct_answer' => $qAnsCor, 'parent_quiz_id' => $quizID, 'question_type' => $questionType, 'question_order' => $questionOrder, 'question_answer_type' => $qAnsType, 'question_answer_hint' => stripslashes($qAnsHint), 'question_answer_file_types' => $qAnsFileTypes);
                }
            }
        }
        // end if question found.
    }
    // #### 5 - Check we have existing questions to save
    if (count($questionsToSave)) {
        // Now save all data back to the database.
        foreach ($questionsToSave as $questionID => $questionDetails) {
            $wpdb->query(arrayToSQLUpdate($wpcwdb->quiz_qs, $questionDetails, 'question_id'));
        }
    }
    // #### 6 - Save the new questions we have
    if (count($questionsToSave_New)) {
        // Now save all data back to the database.
        foreach ($questionsToSave_New as $questionDetails) {
            $wpdb->query(arrayToSQLInsert($wpcwdb->quiz_qs, $questionDetails));
        }
    }
    // Show an error if questions are missing details.
    $page->showMessage(__('Questions were successfully updated.', 'wp_courseware'));
}
Example #3
0
/**
 * 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;
}
/**
 * Handle saving questions to the database.
 * 
 * @param Integer $quizID The quiz for which the questions apply to. 
 * @param Boolean $singleQuestionMode If true, then we're updating a single question, and we do things slightly differently.
 */
function WPCW_handler_questions_processSave($quizID, $singleQuestionMode = false)
{
    global $wpdb, $wpcwdb;
    $wpdb->show_errors();
    $questionsToSave = array();
    $questionsToSave_New = array();
    // Check $_POST data for the
    foreach ($_POST as $key => $value) {
        // #### 1 - Check if we're deleting a question from this quiz
        // We're not just deleting the question, just the association. This is because questions remain in the
        // pool now.
        if (preg_match('/^delete_wpcw_quiz_details_([0-9]+)$/', $key, $matches)) {
            // Remove mapping from the mapping table.
            $SQL = $wpdb->prepare("\n\t\t\t\tDELETE FROM {$wpcwdb->quiz_qs_mapping}\n\t\t\t\tWHERE question_id = %d\n\t\t\t\t  AND parent_quiz_id = %d\n\t\t\t", $matches[1], $quizID);
            $wpdb->query($SQL);
            // Update usage counts
            WPCW_questions_updateUsageCount($matches[1]);
            // Just a deletion - move on to next array item to save processing time.
            continue;
        }
        // #### 2 - See if we have a question to check for.
        if (preg_match('/^question_question_(new_question_)?([0-9]+)$/', $key, $matches)) {
            // Got the ID of the question, now get answers and correct answer.
            $questionID = $matches[2];
            // Store the extra string if we're adding a new question.
            $newQuestionPrefix = $matches[1];
            $fieldName_Answers = 'question_answer_' . $newQuestionPrefix . $questionID;
            $fieldName_Answers_Img = 'question_answer_image_' . $newQuestionPrefix . $questionID;
            $fieldName_Correct = 'question_answer_sel_' . $newQuestionPrefix . $questionID;
            $fieldName_Type = 'question_type_' . $newQuestionPrefix . $questionID;
            $fieldName_Order = 'question_order_' . $newQuestionPrefix . $questionID;
            $fieldName_AnswerType = 'question_answer_type_' . $newQuestionPrefix . $questionID;
            $fieldName_AnswerHint = 'question_answer_hint_' . $newQuestionPrefix . $questionID;
            $fieldName_Explanation = 'question_answer_explanation_' . $newQuestionPrefix . $questionID;
            $fieldName_Image = 'question_image_' . $newQuestionPrefix . $questionID;
            // For Multi-Choice - Answer randomization
            $fieldName_Multi_Random_Enable = 'question_multi_random_enable_' . $newQuestionPrefix . $questionID;
            $fieldName_Multi_Random_Count = 'question_multi_random_count_' . $newQuestionPrefix . $questionID;
            // Order should be a number
            $questionOrder = 0;
            if (isset($_POST[$fieldName_Order])) {
                $questionOrder = $_POST[$fieldName_Order] + 0;
            }
            // Default types
            $qAns = false;
            $qAnsCor = false;
            $qAnsType = false;
            // Just used for open question types.
            $qAnsFileTypes = false;
            // Just used for upload file types.
            // Get the hint - Just used for open and upload types. Allow HTML.
            $qAnsHint = trim(WPCW_arrays_getValue($_POST, $fieldName_AnswerHint));
            // Get the explanation - All questions. Allow HTML.
            $qAnsExplain = trim(WPCW_arrays_getValue($_POST, $fieldName_Explanation));
            // The image URL to use. No HTML. Table record is 300 chars, hence cropping.
            $qQuesImage = trim(substr(strip_tags(WPCW_arrays_getValue($_POST, $fieldName_Image)), 0, 300));
            // How many questions are there is this selection? 1 by default for non-random questions.
            $expandedQuestionCount = 1;
            // For Multi-Choice - Answer randomization
            $qMultiRandomEnable = false;
            $qMultiRandomCount = 5;
            // What type of question do we have?
            $questionType = WPCW_arrays_getValue($_POST, $fieldName_Type);
            switch ($questionType) {
                case 'multi':
                    $qAns = WPCW_quiz_MultipleChoice::editSave_extractAnswerList($fieldName_Answers, $fieldName_Answers_Img);
                    $qAnsCor = WPCW_quiz_MultipleChoice::editSave_extractCorrectAnswer($qAns, $fieldName_Correct);
                    // Provide the UI with at least once slot for an answer.
                    if (!$qAns) {
                        $qAns = array('1' => array('answer' => ''), '2' => array('answer' => ''));
                    }
                    // Check randomization values (boolean will be 'on' to enable, as it's a checkbox)
                    $qMultiRandomEnable = 'on' == WPCW_arrays_getValue($_POST, $fieldName_Multi_Random_Enable);
                    $qMultiRandomCount = intval(WPCW_arrays_getValue($_POST, $fieldName_Multi_Random_Count));
                    break;
                case 'open':
                    // See if there's a question type that's been sent back to the server.
                    $answerTypes = WPCW_quiz_OpenEntry::getValidAnswerTypes();
                    $thisAnswerType = WPCW_arrays_getValue($_POST, $fieldName_AnswerType);
                    // Validate the answer type is in the list. Don't create a default so that user must choose.
                    if (isset($answerTypes[$thisAnswerType])) {
                        $qAnsType = $thisAnswerType;
                    }
                    // There's no correct answer for an open question.
                    $qAnsCor = false;
                    break;
                case 'upload':
                    $fieldName_FileType = 'question_answer_file_types_' . $newQuestionPrefix . $questionID;
                    // Check new file extension types, parsing them.
                    $qAnsFileTypesRaw = WPCW_files_cleanFileExtensionList(WPCW_arrays_getValue($_POST, $fieldName_FileType));
                    $qAnsFileTypes = implode(',', $qAnsFileTypesRaw);
                    break;
                case 'truefalse':
                    $qAnsCor = WPCW_quiz_TrueFalse::editSave_extractCorrectAnswer($fieldName_Correct);
                    break;
                    // Validate the the JSON data here... ensure all the tags are valid (not worried about the counts).
                    // Then save back to database.
                // Validate the the JSON data here... ensure all the tags are valid (not worried about the counts).
                // Then save back to database.
                case 'random_selection':
                    // Reset to zero for counting below.
                    $expandedQuestionCount = 0;
                    $decodedTags = WPCW_quiz_RandomSelection::decodeTagSelection(stripslashes($value));
                    // Capture just ID and count and resave back to database.
                    $toSaveList = false;
                    if (!empty($decodedTags)) {
                        $toSaveList = array();
                        foreach ($decodedTags as $decodedKey => $decodedDetails) {
                            $toSaveList[$decodedKey] = $decodedDetails['count'];
                            // Track requested questions
                            $expandedQuestionCount += $decodedDetails['count'];
                        }
                    }
                    // Overwrite $value to use cleaned question
                    $value = json_encode($toSaveList);
                    break;
                    // Not expecting anything here... so not handling the error case.
                // Not expecting anything here... so not handling the error case.
                default:
                    break;
            }
            // ### 4a - Encode the answer data
            $encodedqAns = $qAns;
            if (!empty($qAns)) {
                foreach ($encodedqAns as $idx => $data) {
                    $encodedqAns[$idx]['answer'] = base64_encode($data['answer']);
                }
            }
            // ### 4b - Save new question data as a list ready for saving to the database.
            $quDataToSave = array('question_answers' => false, 'question_question' => stripslashes($value), 'question_data_answers' => serialize($encodedqAns), 'question_correct_answer' => $qAnsCor, 'question_type' => $questionType, 'question_order' => $questionOrder, 'question_answer_type' => $qAnsType, 'question_answer_hint' => stripslashes($qAnsHint), 'question_answer_explanation' => stripslashes($qAnsExplain), 'question_answer_file_types' => $qAnsFileTypes, 'question_image' => $qQuesImage, 'question_expanded_count' => $expandedQuestionCount, 'question_multi_random_enable' => $qMultiRandomEnable, 'question_multi_random_count' => $qMultiRandomCount, 'taglist' => array());
            // ### 5 - Check if there are any tags to save. Only happens for questions that
            // haven't been saved, so that we can save when we do a $_POST save.
            $tagFieldForNewQuestions = 'tags_to_add_' . $newQuestionPrefix . $questionID;
            if (isset($_POST[$tagFieldForNewQuestions])) {
                if (!empty($_POST[$tagFieldForNewQuestions])) {
                    // Validate each tag ID we have, add to list to be stored for this question later.
                    foreach ($_POST[$tagFieldForNewQuestions] as $idx => $tagText) {
                        $tagText = trim(stripslashes($tagText));
                        if ($tagText) {
                            $quDataToSave['taglist'][] = $tagText;
                        }
                    }
                }
            }
            // Not a new question - so not got question ID as yet
            if ($newQuestionPrefix) {
                $questionsToSave_New[] = $quDataToSave;
            } else {
                $quDataToSave['question_id'] = $questionID;
                $questionsToSave[$questionID] = $quDataToSave;
            }
        }
        // end if question found.
    }
    // Only need to adjust quiz settings when editing a quiz and not a single question.
    if (!$singleQuestionMode) {
        // #### 6 - Remove association of all questions for this quiz
        //          as we're going to re-add them.
        $wpdb->query($wpdb->prepare("\n\t\t\t\t\tDELETE FROM {$wpcwdb->quiz_qs_mapping}\n\t\t\t\t\tWHERE parent_quiz_id = %d\n\t\t\t\t", $quizID));
    }
    // #### 7 - Check we have existing questions to save
    if (count($questionsToSave)) {
        // Now save all data back to the database.
        foreach ($questionsToSave as $questionID => $questionDetails) {
            // Extract the question order, as can't save order with question in DB
            $questionOrder = $questionDetails['question_order'];
            unset($questionDetails['question_order']);
            // Tag list only used for new questions, so remove this field
            unset($questionDetails['taglist']);
            // Save question details back to database.
            $wpdb->query(arrayToSQLUpdate($wpcwdb->quiz_qs, $questionDetails, 'question_id'));
            // No need to update counts/associations when editing a single lone question
            if (!$singleQuestionMode) {
                // Create the association for this quiz/question.
                $wpdb->query($wpdb->prepare("\n\t\t\t\t\tINSERT INTO {$wpcwdb->quiz_qs_mapping} \n\t\t\t\t\t(question_id, parent_quiz_id, question_order)\n\t\t\t\t\tVALUES (%d, %d, %d)\n\t\t\t\t", $questionID, $quizID, $questionOrder));
                // Update usage count for question.
                WPCW_questions_updateUsageCount($questionID);
            }
        }
    }
    // #### 8 - Save the new questions we have
    if (count($questionsToSave_New)) {
        // Now save all data back to the database.
        foreach ($questionsToSave_New as $questionDetails) {
            // Extract the question order, as can't save order with question in DB
            $questionOrder = $questionDetails['question_order'];
            unset($questionDetails['question_order']);
            // Extract the tags added for this question - we'll save manually.
            $tagsToAddList = $questionDetails['taglist'];
            unset($questionDetails['taglist']);
            // Create question in database
            $wpdb->query(arrayToSQLInsert($wpcwdb->quiz_qs, $questionDetails));
            $newQuestionID = $wpdb->insert_id;
            // No need to update counts/associations when editing a single lone question
            if (!$singleQuestionMode) {
                // Create the association for this quiz/question.
                $wpdb->query($wpdb->prepare("\n\t\t\t\t\tINSERT INTO {$wpcwdb->quiz_qs_mapping} \n\t\t\t\t\t(question_id, parent_quiz_id, question_order)\n\t\t\t\t\tVALUES (%d, %d, %d)\n\t\t\t\t", $newQuestionID, $quizID, $questionOrder));
                // Update usage
                WPCW_questions_updateUsageCount($newQuestionID);
            }
            // Add associations for tags for this unsaved question now we finally have a question ID.
            if (!empty($tagsToAddList)) {
                WPCW_questions_tags_addTags($newQuestionID, $tagsToAddList);
            }
        }
    }
}