diff --git a/quiz.install b/quiz.install index d910607..ba9b4ed 100644 --- a/quiz.install +++ b/quiz.install @@ -222,6 +222,12 @@ function quiz_schema() { 'not null' => TRUE, 'default' => 0, ), + 'single_page' => array( + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + 'default' => 0, + ), ), 'primary key' => array('vid'), // 'unique keys' => array('vid'), @@ -789,6 +795,18 @@ function quiz_update_7407(&$sandbox) { return t('Added new auto update max score field to the quiz_node_relationship table'); } +/** + * Add single page column. + */ +function quiz_update_7408(&$sandbox) { + $spec = array( + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + 'default' => 0, + ); + db_add_field('quiz_node_properties', 'single_page', $spec); +} /** diff --git a/quiz.module b/quiz.module index ab679e8..784d128 100644 --- a/quiz.module +++ b/quiz.module @@ -231,6 +231,7 @@ function quiz_access_my_results($quiz) { * True if access, false otherwise */ function quiz_access_my_result($rid) { + global $user; if (!user_access('view own quiz results')) { return FALSE; } @@ -544,7 +545,7 @@ function quiz_theme($existing, $type, $theme, $path) { 'file' => 'quiz.pages.inc', ), 'quiz_progress' => array( - 'variables' => array('question_number' => NULL, 'num_questions' => NULL, 'allow_jumping' => NULL, 'time_limit' => NULL), + 'variables' => array('question_number' => NULL, 'num_questions' => NULL, 'allow_jumping' => NULL, 'time_limit' => NULL, 'quiz' => NULL), 'file' => 'quiz.pages.inc', ), 'quiz_no_feedback' => array( @@ -559,6 +560,10 @@ function quiz_theme($existing, $type, $theme, $path) { 'file' => 'quiz.pages.inc', 'variables' => array('question_node' => NULL), ), + 'quiz_multi_question_node' => array( + 'file' => 'quiz.pages.inc', + 'variables' => array('question_node' => NULL), + ), 'question_selection_table' => array( 'file' => 'quiz.admin.inc', 'render element' => 'form', @@ -734,7 +739,8 @@ function quiz_insert($node) { 'allow_resume' => $node->allow_resume, 'allow_jumping' => $node->allow_jumping, 'show_passed' => $node->show_passed, - 'mark_doubtful' => $node->mark_doubtful + 'mark_doubtful' => $node->mark_doubtful, + 'single_page' => $node->single_page, )) ->execute(); @@ -792,7 +798,8 @@ function quiz_update($node) { 'allow_resume' => $node->allow_resume, 'allow_jumping' => $node->allow_jumping, 'show_passed' => $node->show_passed, - 'mark_doubtful' => $node->mark_doubtful + 'mark_doubtful' => $node->mark_doubtful, + 'single_page' => $node->single_page, )) ->condition('vid', $node->vid) ->condition('nid', $node->nid) @@ -939,6 +946,7 @@ function _quiz_get_node_defaults() { 'quiz_open' => _quiz_form_prepare_date(), 'quiz_close' => _quiz_form_prepare_date(NULL, variable_get('quiz_default_close', 30)), 'mark_doubtful' => 0, + 'single_page' => 0, ); } @@ -1169,6 +1177,12 @@ function quiz_form(&$node, &$form_state) { '#description' => t('The difference between "random order" and "random questions" is that with "random questions" questions are drawn randomly from a pool of questions. With "random order" the quiz will always consist of the same questions. With "Categorized random questions" you can choose several terms questions should be drawn from, and you can also choose how many questions that should be drawn from each, and max score for each term.'), '#default_value' => $node->randomization, ); + $form['taking']['single_page'] = array( + '#type' => 'checkbox', + '#title' => t('Show all questions on a single page'), + '#default_value' => $node->single_page, + '#description' => t('Whether to show all questions on a single page in the @quiz', array('@quiz' => QUIZ_NAME)), + ); $form['taking']['feedback'] = array( '#type' => 'fieldset', '#title' => t('Feedback'), @@ -1893,7 +1907,7 @@ function quiz_take_quiz($quiz) { $rid = $user->uid > 0 ? _quiz_active_result_id($user->uid, $quiz->nid, $quiz->vid) : 0; // Are we resuming an in-progress quiz? - if ($quiz->allow_resume && $rid > 0) { + if (!$quiz->single_page && $quiz->allow_resume && $rid > 0) { _quiz_resume_existing_quiz($quiz, $user->uid, $rid); } @@ -1907,9 +1921,21 @@ function quiz_take_quiz($quiz) { //return array('body' => array('#markup' => t('This quiz is closed'))); } } + + //initialization for multipage + if ($quiz->single_page) { + if (!isset($_SESSION['quiz_' . $quiz->nid]['quiz_questions'])) { + _quiz_take_quiz_init($quiz); + } + $questions = quiz_build_question_list($quiz); + $question_nodes = array(); + foreach($questions as $question) { + $question_nodes[] = node_load($question['nid'], $question['vid']); + } + } $q_passed_validation = FALSE; - + $repeat_until_correct_error = FALSE; if (quiz_availability($quiz) !== TRUE) { drupal_set_message(t('This quiz is not available anymore.'), 'error'); return array('body' => array('#markup' => t('This quiz is closed'))); @@ -1918,11 +1944,11 @@ function quiz_take_quiz($quiz) { if (isset($_SESSION['quiz_' . $quiz->nid]['question_duration'])) { $_SESSION['quiz_' . $quiz->nid]['question_duration'] -= REQUEST_TIME - $_SESSION['quiz_' . $quiz->nid]['question_start_time']; } - + if (!isset($_POST['op'])) { // @todo Starting new quiz... Do we need to show instructions here? } - elseif (isset($_POST['question_nid']) && ($_POST['question_nid'] != $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['nid'])) { + elseif (!$quiz->single_page && (isset($_POST['question_nid']) && $_POST['question_nid'] != $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['nid'])) { // The user has pressed the navigation buttons multiple times... } elseif (isset($_SESSION['quiz_' . $quiz->nid]['question_duration']) && $_SESSION['quiz_' . $quiz->nid]['question_duration'] < -2) { @@ -1930,7 +1956,7 @@ function quiz_take_quiz($quiz) { } // Workaround to show question and feedback in separage pages, when the // feedback time it set to show after each questions - elseif ($_POST['op'] == t('Next question')) { + elseif ($_POST['op'] == t('Continue')) { drupal_goto("node/{$quiz->nid}/take"); } // We maintain two lists: previous questions and upcomming questions. @@ -1948,45 +1974,72 @@ function quiz_take_quiz($quiz) { $_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions'][] = $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]; $former_question_array = array_shift($_SESSION['quiz_' . $quiz->nid]['quiz_questions']); $former_question = node_load($former_question_array['nid'], $former_question_array['vid']); - - // Call hook_evaluate_question(). - $types = _quiz_get_question_types(); - $module = $types[$former_question->type]['module']; - $result = module_invoke($module, 'evaluate_question', $former_question, $_SESSION['quiz_' . $quiz->nid]['result_id']); - $q_passed_validation = $result->is_valid; - $check_jump = TRUE; - if ($q_passed_validation === TRUE) { - quiz_store_question_result($quiz, $result, array('set_msg' => TRUE, 'question_data' => $former_question_array)); - } - elseif ($quiz->allow_jumping && _quiz_is_int($_POST['jump_to_question'])) { - $_POST['op'] = t('Leave blank'); - $allow_skipping = TRUE; - $jumping = TRUE; + if (!$quiz->single_page) { + // Call hook_evaluate_question(). + $types = _quiz_get_question_types(); + $module = $types[$former_question->type]['module']; + $result = module_invoke($module, 'evaluate_question', $former_question, $_SESSION['quiz_' . $quiz->nid]['result_id']); + $q_passed_validation = $result->is_valid; + $check_jump = TRUE; + if ($q_passed_validation === TRUE) { + quiz_store_question_result($quiz, $result, array('set_msg' => TRUE, 'question_data' => $former_question_array)); + } + elseif ($quiz->allow_jumping && _quiz_is_int($_POST['jump_to_question'])) { + $_POST['op'] = t('Leave blank'); + $allow_skipping = TRUE; + $jumping = TRUE; + } } - - // Stash feedback in the session, since the $_POST gets cleared. - if ($quiz->feedback_time == QUIZ_FEEDBACK_QUESTION && $_POST['op'] != t('Back') && $q_passed_validation === TRUE) { - // Invoke hook_get_report(). - $report = module_invoke($module, 'get_report', $former_question_array['nid'], $former_question_array['vid'], $_SESSION['quiz_' . $quiz->nid]['result_id']); - $path = drupal_get_path('module', 'quiz'); - require_once DRUPAL_ROOT . '/' . $path . '/quiz.pages.inc'; - if ($report) { - $report_form = drupal_get_form('quiz_report_form', array($report), TRUE, TRUE, TRUE); - $report_form['op'] = array( - '#type' => 'submit', - '#value' => t('Next question'), - ); - return $report_form; + else { + //storage of all the validation errors + $results = array(); + $quiz_end = TRUE; + $_SESSION['quiz_' . $quiz->nid]['previous_result_id'] = $_SESSION['quiz_' . $quiz->nid]['result_id']; + + foreach ($question_nodes as $question_node) { + $former_question = node_load($question_node->nid, $question_node->vid); + $_POST['tries'] = isset($_POST['tries_' . $question_node->nid]) ? $_POST['tries_' . $question_node->nid] : NULL; + // Call hook_evaluate_question() for each question. + $types = _quiz_get_question_types(); + $module = $types[$former_question->type]['module']; + $results[$question_node->nid] = module_invoke($module, 'evaluate_question', $former_question, $_SESSION['quiz_' . $quiz->nid]['result_id']); + $q_passed_validation = $results[$question_node->nid]->is_valid; + + // If validation passes + if ($results[$question_node->nid]->is_valid === TRUE) { + quiz_store_question_result($quiz, $results[$question_node->nid], array('set_msg' => TRUE, 'question_data' => $former_question_array)); + } + // If validation didn't pass + else { + // takes all the validation errors and put it into a single string because thats what the error handling code expects + $q_passed_validations = t('Please fix the errors in your submission.'); + $quiz_end = FALSE; + } } } - + if ($quiz->repeat_until_correct && $_POST['op'] != t('Back') && $q_passed_validation === TRUE) { // If the question was answered incorrectly, repeat it - if ($result && !$result->is_correct && $result->is_evaluated) { - $last_q = array_pop($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions']); - array_unshift($_SESSION['quiz_' . $quiz->nid]['quiz_questions'], $last_q); - drupal_set_message(t('The answer was incorrect. Please try again.'), 'error'); - unset($_SESSION['quiz_' . $quiz->nid]['feedback']); + if(!$quiz->single_page) { + if ($result && !$result->is_correct && $result->is_evaluated) { + $last_q = array_pop($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions']); + array_unshift($_SESSION['quiz_' . $quiz->nid]['quiz_questions'], $last_q); + drupal_set_message(t('The answer was incorrect. Please try again.'), 'error'); + $repeat_until_correct_error = TRUE; + unset($_SESSION['quiz_' . $quiz->nid]['feedback']); + } + } + // Handler for single page repeat until correct + else { + foreach ($results as $result) { + if ($result && !$result->is_correct && $result->is_evaluated) { + drupal_set_message(t('An answer was incorrect. Please try again.'), 'error'); + $repeat_until_correct_error = TRUE; + $quiz_end = FALSE; + unset($_SESSION['quiz_' . $quiz->nid]['feedback']); + break; + } + } } } elseif ($_POST['op'] == t('Back') && $quiz->backwards_navigation) { @@ -1998,9 +2051,50 @@ function quiz_take_quiz($quiz) { array_unshift($_SESSION[$quiz_id]['quiz_questions'], $last_q); } } + + // Stash feedback in the session, since the $_POST gets cleared. + if ($quiz->feedback_time == QUIZ_FEEDBACK_QUESTION && $_POST['op'] != t('Back') && $q_passed_validation === TRUE && $repeat_until_correct_error === FALSE) { + if(!$quiz->single_page) { + // Invoke hook_get_report(). + $report = module_invoke($module, 'get_report', $former_question_array['nid'], $former_question_array['vid'], $_SESSION['quiz_' . $quiz->nid]['result_id']); + $path = drupal_get_path('module', 'quiz'); + require_once DRUPAL_ROOT . '/' . $path . '/quiz.pages.inc'; + if ($report) { + $report_form = drupal_get_form('quiz_report_form', array($report), TRUE, TRUE, TRUE); + $report_form['op'] = array( + '#type' => 'submit', + '#value' => t('Continue'), + ); + return $report_form; + } + } + else { + $path = drupal_get_path('module', 'quiz'); + require_once DRUPAL_ROOT . '/' . $path . '/quiz.pages.inc'; + $_SESSION['quiz_' . $quiz->nid]['quiz_questions'] = array(); + + $form = array(); + foreach($question_nodes as $question_node) { + $report = module_invoke($module, 'get_report', $question_node->nid, $question_node->vid, $_SESSION['quiz_' . $quiz->nid]['result_id']); + if($report) { + $report_form = drupal_get_form('quiz_report_form', array($report), TRUE, TRUE, TRUE); + } + $form[] = $report_form; + } + $form[count($form)-1]['op'] = array( + '#type' => 'submit', + '#value' => t('Continue'), + ); + return $form; + + } + } + // If anonymous user, refresh url with unique hash to prevent caching. if (!$user->uid && $q_passed_validation === TRUE) { - drupal_goto('node/' . $quiz->nid . '/take', array('query' => array('quizkey' => md5(mt_rand() . REQUEST_TIME)))); + if (!$quiz->single_page) { + drupal_goto('node/' . $quiz->nid . '/take', array('query' => array('quizkey' => md5(mt_rand() . REQUEST_TIME)))); + } } } // Check for a skip. @@ -2029,7 +2123,7 @@ function quiz_take_quiz($quiz) { // Store that the question was skipped: quiz_store_question_result($quiz, $result, array('set_msg' => TRUE, 'question_data' => $former_question_array)); } - + if (isset($check_jump) && $check_jump) { if ($quiz->allow_jumping && _quiz_is_int($_POST['jump_to_question'])) { quiz_jump_to($_POST['jump_to_question'], $quiz, $_SESSION['quiz_' . $quiz->nid]['result_id']); @@ -2042,13 +2136,25 @@ function quiz_take_quiz($quiz) { // If we got no error when validating the question if (!is_string($q_passed_validation) || $_POST['op'] == t('Back') && $quiz->backwards_navigation) { - $question_node = node_load( - $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['nid'], - $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['vid'] - ); - if (isset($_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['rid'])) { - $question_node->rid = $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['rid']; + if(!$quiz->single_page) { + $question_node = node_load( + $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['nid'], + $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['vid'] + ); + if (isset($_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['rid'])) { + $question_node->rid = $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['rid']; + } } + else { + if($question_nodes) { + foreach($question_nodes as $question_node) { + if (isset($_SESSION['quiz_' . $quiz->nid]['previous_result_id'])) { + $question_node->rid = $_SESSION['quiz_' . $quiz->nid]['previous_result_id']; + } + } + } + } + // We got an error message when trying to validate the previous answer } else { @@ -2074,7 +2180,8 @@ function quiz_take_quiz($quiz) { 'question_number' => $question_number, 'num_questions' => $number_of_questions, 'allow_jumping' => $quiz->allow_jumping, - 'time_limit' => $quiz->time_limit)); + 'time_limit' => $quiz->time_limit, + 'quiz' => $quiz)); $content['progress']['#weight'] = -50; if (count($_SESSION['quiz_' . $quiz->nid]['quiz_questions']) + count($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions']) > $number_of_questions) { drupal_set_message(t('At least one question have been deleted from the quiz after you started taking it. You will have to start over.'), 'warning', FALSE); @@ -2140,7 +2247,12 @@ function quiz_take_quiz($quiz) { // If we're not yet at the end. if (empty($quiz_end)) { - $content['body']['question']['#markup'] = quiz_take_question_view($question_node, $quiz); + if (!$quiz->single_page) { + $content['body']['question']['#markup'] = quiz_take_question_view($question_node, $quiz); + } + else { + $content['body']['question']['#markup'] = quiz_node_view_multi($question_nodes, $quiz); + } $content['body']['question']['#weight'] = 0; // If we had feedback from the last question. if (isset($_SESSION['quiz_' . $quiz->nid]['feedback']) && $quiz->feedback_time == QUIZ_FEEDBACK_QUESTION) { @@ -2221,6 +2333,7 @@ function quiz_jump_to($question_num, $quiz, $rid) { function _quiz_take_quiz_init($quiz) { // Create question list. $questions = quiz_build_question_list($quiz); + if ($questions === FALSE) { drupal_set_message(t('Not enough random questions were found. Please add more questions before trying to take this @quiz.', array('@quiz' => QUIZ_NAME)), 'error'); return FALSE; @@ -2230,7 +2343,7 @@ function _quiz_take_quiz_init($quiz) { drupal_set_message(t('No questions were found. Please !assign_questions before trying to take this @quiz.', array('@quiz' => QUIZ_NAME, '!assign_questions' => l(t('assign questions'), 'node/' . $quiz->nid . '/questions'))), 'error'); return FALSE; } - + // Initialize session variables. $_SESSION['quiz_' . $quiz->nid]['result_id'] = quiz_create_rid($quiz); $_SESSION['quiz_' . $quiz->nid]['quiz_questions'] = $questions; @@ -2238,6 +2351,7 @@ function _quiz_take_quiz_init($quiz) { $_SESSION['quiz_' . $quiz->nid]['question_number'] = 0; $_SESSION['quiz_' . $quiz->nid]['question_start_time'] = REQUEST_TIME; $_SESSION['quiz_' . $quiz->nid]['quiz_vid'] = $quiz->vid; + $_SESSION['current_quiz_id'] = $quiz->nid; if ($quiz->time_limit > 0) { $_SESSION['quiz_' . $quiz->nid]['question_duration'] = $quiz->time_limit; @@ -2264,6 +2378,25 @@ function quiz_take_question_view($question_node, $quiz_node) { } /** + * Create the view for a multi-question the user is about to take. + * + * @param $question_nodes + * The question nodes that should be rendered. + * @param $quiz_node + * The quiz node. + * @return + * A string containing the body of the node. + */ +function quiz_node_view_multi($question_nodes = array(), $quiz_node) { + foreach ($question_nodes as &$question_node) { + node_build_content($question_node, 'teaser'); + module_invoke_all('node_build_alter', $question_node, FALSE); + $question_node->body = drupal_render($question_node->content); + } + return theme('quiz_multi_question_node', array('question_node' => $question_nodes)); +} + +/** * Store a quiz question result. * * @param $quiz @@ -2654,17 +2787,17 @@ function quiz_update_max_score_properties($quizzes_to_update) { if (!empty($results_to_update)) { db_update('quiz_node_results') ->expression('score', - 'ROUND( - 100 * ( - SELECT COALESCE (SUM(a.points_awarded), 0) - FROM {quiz_node_results_answers} a - WHERE a.result_id = {quiz_node_results}.result_id - ) / ( - SELECT max_score - FROM {quiz_node_properties} qnp - WHERE qnp.vid = {quiz_node_results}.vid - ) - )') + 'ROUND( + 100 * ( + SELECT COALESCE (SUM(a.points_awarded), 0) + FROM {quiz_node_results_answers} a + WHERE a.result_id = {quiz_node_results}.result_id + ) / ( + SELECT max_score + FROM {quiz_node_properties} qnp + WHERE qnp.vid = {quiz_node_results}.vid + ) + )') ->condition('vid', $results_to_update, 'IN') ->execute(); } @@ -3215,6 +3348,7 @@ function _quiz_resume_existing_quiz($quiz, $uid, $rid) { if ($quiz->time_limit > 0) { $_SESSION['quiz_' . $quiz->nid]['question_duration'] = $quiz->time_limit; } + drupal_set_message(t('Resuming a previous quiz in-progress.'), 'status'); } diff --git a/quiz.pages.inc b/quiz.pages.inc index c16144a..ebdb5d0 100644 --- a/quiz.pages.inc +++ b/quiz.pages.inc @@ -470,6 +470,7 @@ function theme_quiz_score_incorrect() { function theme_quiz_progress($variables) { $question_number = $variables['question_number']; $num_of_question = $variables['num_questions']; + $quiz = $variables['quiz']; // TODO Number of parameters in this theme funcion does not match number of parameters found in hook_theme. // Determine the percentage finished (not used, but left here for other implementations). //$progress = ($question_number*100)/$num_of_question; @@ -483,7 +484,12 @@ function theme_quiz_progress($variables) { $output = ''; $output .= '
'; - $output .= t('Question !x of @y', array('!x' => $current_question, '@y' => $num_of_question)); + if ($quiz->single_page) { + $output .= format_plural($num_of_question, '1 question', '@count questions'); + } + else { + $output .= t('Question %x of %y', array('%x' => $current_question, '%y' => $num_of_question)); + } $output .= '
' . "\n"; // Add div to be used by jQuery countdown if ($variables['time_limit']) { @@ -568,7 +574,7 @@ function theme_quiz_take_summary($variables) { $output .= '
' . $summary['result'] . '
' . "\n"; } // Get the feedback for all questions. These are included here to provide maximum flexibility for themers - if ($quiz->display_feedback) { + if ($quiz->display_feedback || ($quiz->feedback_time == QUIZ_FEEDBACK_END && $quiz->pass_rate > 0 && $score['percentage_score'] >= $quiz->pass_rate)) { $form = drupal_get_form('quiz_report_form', $questions); $output .= drupal_render($form); } @@ -658,6 +664,42 @@ function theme_quiz_single_question_node($variables) { } /** + * Theme the multi question node. + * + * @param $nodes + * The question nodes + * @return + * Themed html feedback + */ +function theme_quiz_multi_question_node($variables) { + $nodes = $variables['question_node']; + $qno = 0; + + foreach ($nodes as $node) { + $qno++; + if (isset($node)) { + preg_match('|]*?>(.*?)|si', $node->body, $matches); + $form_free_body = preg_replace('/]*?>/', '', $matches[1]); + $item_list[] = array( + 'data' => str_replace('name="tries', 'name="tries_' . $node->nid . '', $form_free_body), + ); + } + else { + break; + } + } + $form_a = '
'; + $form_b = '
'; + $body = array( + array('#markup' => $form_a), + array('#theme' => 'item_list', '#items' => $item_list), + array('#markup' => $form_b), + ); + + return drupal_render($body); +} + +/** * Theme the stats on the views page * * @param $node