Index: quiz.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/quiz/quiz.install,v retrieving revision 1.5 diff -u -r1.5 quiz.install --- quiz.install 13 Apr 2007 20:33:38 -0000 1.5 +++ quiz.install 21 Jun 2007 09:11:00 -0000 @@ -1,166 +1,209 @@ 'nid', + '{quiz}' => 'nid', + '{quiz_questions}' => 'quiz_nid', + '{quiz_results}' => 'quiz_nid' + ); + + $result = db_query("SELECT nid FROM {node} WHERE type = 'quiz'"); + while ($node = db_fetch_array($result)) { + // Remove question results pertaining to this quiz. This alone could warrant + // storing quiz_nid in the question_results table. + $rid_result = db_query("SELECT rid FROM {quiz_results} WHERE quiz_nid = %d", $node['nid']); + while ($row = db_fetch_array($rid_result)) { + db_query("DELETE FROM {quiz_question_results} WHERE rid = %d", $row['rid']); + } + + _quiz_purge_database($table_field_array, $node['nid']); + } + db_query("DELETE FROM {node} WHERE type = 'quiz'"); + + db_query("DROP TABLE {quiz_question_results}"); + db_query("DROP TABLE {quiz_results}"); + db_query("DROP TABLE {quiz_question_answer}"); + db_query("DROP TABLE {quiz_questions}"); + db_query("DROP TABLE {quiz_question}"); + db_query("DROP TABLE {quiz}"); + db_query("DELETE FROM {sequences} WHERE name = '{quiz_results}_rid'"); + + variable_del('quiz'); + + drupal_set_message(t('Quiz module: Uninstallation script complete.')); +} + +/** + * Helper function for the uninstallation script. + * + * @param $table_field_array + * Associative array of table and field names. + * @param $value + * The argument for the field which is to be deleted from the table. + * @param $type + * The type of the argument: %d, '%s' etc. + */ +function _quiz_purge_database($table_field_array, $value, $type = '%d') { + foreach ($table_field_array as $table => $field) { + db_query("DELETE FROM $table WHERE $field = $type", $value); + } } Index: multichoice.info =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/quiz/multichoice.info,v retrieving revision 1.3.2.2 diff -u -r1.3.2.2 multichoice.info --- multichoice.info 18 Jun 2007 23:06:59 -0000 1.3.2.2 +++ multichoice.info 21 Jun 2007 09:10:59 -0000 @@ -1,5 +1,6 @@ -; $Id: multichoice.info,v 1.3.2.2 2007/06/18 23:06:59 dww Exp $ +; $Id: multichoice.info 231 2007-06-20 07:46:11Z karthik $ name = Multichoice package = Quiz -description = Multiple choice question type +description = Multiple choice question type. dependencies = quiz + Index: quiz_datetime.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/quiz/quiz_datetime.inc,v retrieving revision 1.2 diff -u -r1.2 quiz_datetime.inc --- quiz_datetime.inc 13 Apr 2007 20:33:38 -0000 1.2 +++ quiz_datetime.inc 21 Jun 2007 09:11:00 -0000 @@ -1,24 +1,28 @@ $date; - // If we have all the parameters, re-calculate $node->event_$date . + // If we have all the parameters, recalculate $node->event_$date. if (isset($prefix['year']) && isset($prefix['month']) && isset($prefix['day'])) { // Build a timestamp based on the date supplied and the configured timezone. $node->$date = _quiz_mktime(0, 0, 0, $prefix['month'], $prefix['day'], $prefix['year'], 0); @@ -33,18 +37,22 @@ * All time values in the database are GMT and translated here prior to insertion. * * Time zone settings are applied in the following order: - * 1. If supplied, time zone offset is applied - * 2. If user time zones are enabled, user time zone offset is applied - * 3. If neither 1 nor 2 apply, the site time zone offset is applied - * - * @param $format The date() format to apply to the timestamp. - * @param $timestamp The GMT timestamp value. - * @param $offset Time zone offset to apply to the timestamp. - * @return gmdate() formatted date value + * 1. If supplied, time zone offset is applied. + * 2. If user time zones are enabled, user time zone offset is applied. + * 3. If neither 1 nor 2 apply, the site time zone offset is applied. + * + * @param $format + * The date() format to apply to the timestamp. + * @param $timestamp + * The GMT timestamp value. + * @param $offset + * Time zone offset to apply to the timestamp. + * @return + * gmdate() formatted date value. */ function _quiz_mktime($hour, $minute, $second, $month, $day, $year, $offset = NULL) { global $user; - //print $user->timezone. " and ". variable_get('date_default_timezone', 0); + $timestamp = gmmktime($hour, $minute, $second, $month, $day, $year); if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) { return $timestamp - $user->timezone; @@ -58,17 +66,21 @@ * Formats a GMT timestamp to local date values using time zone offset supplied. * All timestamp values in event nodes are GMT and translated for display here. * - * Pulled from event + * Pulled from event module. * - * Time zone settings are applied in the following order - * 1. If supplied, time zone offset is applied - * 2. If user time zones are enabled, user time zone offset is applied - * 3. If neither 1 nor 2 apply, the site time zone offset is applied - * - * @param $format The date() format to apply to the timestamp. - * @param $timestamp The GMT timestamp value. - * @param $offset Time zone offset to apply to the timestamp. - * @return gmdate() formatted date value + * Time zone settings are applied in the following order: + * 1. If supplied, time zone offset is applied. + * 2. If user time zones are enabled, user time zone offset is applied. + * 3. If neither 1 nor 2 apply, the site time zone offset is applied. + * + * @param $format + * The date() format to apply to the timestamp. + * @param $timestamp + * The GMT timestamp value. + * @param $offset + * Time zone offset to apply to the timestamp. + * @return + * gmdate() formatted date value. */ function _quiz_date($format, $timestamp, $offset = null) { global $user; @@ -83,7 +95,42 @@ $timestamp += variable_get('date_default_timezone', 0); } - // make sure we apply the site first day of the week setting for dow requests + // Make sure we apply the site first day of the week setting for dow requests. $result = gmdate($format, $timestamp); + return $result; } + +/** + * Takes a time element and prepares to send it to form_date(). + * + * @param $time + * The time to be turned into an array. This can be: + * - A timestamp when from the database. + * - An array (day, month, year) when previewing. + * - Null for new nodes. + * @return + * An array for form_date (day, month, year). + */ +function _quiz_form_prepare_date($time = '', $offset = 0) { + // If this is empty, get the current time. + if ($time == '') { + $time = time(); + $time = strtotime("+$offset days", $time); + } + // If we are previewing, $time will be an array so just pass it through. + $time_array = array(); + if (is_array($time)) { + $time_array = $time; + } + // Otherwise, build the array from the timestamp. + elseif (is_numeric($time)) { + $time_array = array( + 'day' => _quiz_date('j', $time), + 'month' => _quiz_date('n', $time), + 'year' => _quiz_date('Y', $time), + ); + } + + return $time_array; +} Index: multichoice.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/quiz/multichoice.module,v retrieving revision 1.39.2.4 diff -u -r1.39.2.4 multichoice.module --- multichoice.module 14 Jun 2007 03:25:17 -0000 1.39.2.4 +++ multichoice.module 21 Jun 2007 09:10:59 -0000 @@ -1,20 +1,38 @@ 'node/add/multichoice', + 'title' => t(MULTICHOICE_NAME), + 'access' => user_access('create multiple-choice questions') + ); + } + + return $items; +} + +/** * Implementation of hook_perm(). */ function multichoice_perm() { - return array('create multichoice', 'edit own multichoice'); + return array('create multiple-choice questions', 'edit own multiple-choice questions'); } /** @@ -24,11 +42,11 @@ global $user; if ($op == 'create') { - return user_access('create multichoice'); + return user_access('create multiple-choice questions'); } if ($op == 'update' || $op == 'delete') { - if (user_access('edit own multichoice') && ($user->uid == $node->uid)) { + if (user_access('edit own multiple-choice questions') && ($user->uid == $node->uid)) { return TRUE; } } @@ -42,108 +60,107 @@ 'multichoice' => array( 'name' => t(MULTICHOICE_NAME), 'module' => 'multichoice', - 'description' => t('A question type for the quiz module: allows you to create multiple choice questions (ex: A, B, C, D or true/false)'), + 'description' => t('A question type to use in a @quiz: allows you to create multiple-choice questions (e.g. A, B, C, D or True / False).', array('@quiz' => QUIZ_NAME)) ) ); } /** - * Implementation of hook_menu(). - */ -function multichoice_menu($may_cache) { - $items = array(); - if ($may_cache) { - $items[] = array( - 'path' => 'node/add/multichoice', - 'title' => t(MULTICHOICE_NAME), - 'access' => user_access('create multichoice'), - ); - } - return $items; -} - -/** * Implementation of hook_form(). */ function multichoice_form(&$node) { - - // Display multichoice form $form['title'] = array( '#type' => 'textfield', '#title' => t('Title'), '#default_value' => $node->title, '#required' => TRUE, - '#description' => t('Add a title that will help distinguish this question from other questions. This will not be seen during the quiz.'), + '#description' => t('A title that will help distinguish this question from other questions. This will not be seen during the quiz.') ); $form['body'] = array( '#type' => 'textarea', '#title' => t('Question'), '#default_value' => $node->body, - '#required' => TRUE, + '#required' => TRUE ); - - $form['body_filter']['format'] = filter_form($node->format); - $form['multiple_answers'] = array( - '#type' => 'checkbox', - '#title' => t('Multiple answers'), - '#default_value' => $node->multiple_answers, - ); + $form['body_filter']['format'] = filter_form($node->format); - // Determine number of answer rows to display - if (!isset($node->rows)) { - $node->rows = max(2, $node->answers ? count($node->answers) : 5); - } + // Determine number of answer rows to display. The $_POST is used to ensure + // that new rows added during preview are retained if previewed again. + $rows = max(2, $node->answers ? count($node->answers) : 5, count($_POST['answers'])); if ($_POST['more']) { - $node->rows += 5; + $rows += 5; + // Ensure that the 'more' checkbox is unchecked once used. + unset($_POST['more']); } - $answers = $node->answers; - // Display answer rows + // Display answer rows. $form['answers'] = array( '#type' => 'fieldset', '#title' => t('Choices'), '#tree' => TRUE, - '#theme' => 'multichoice_form', + '#theme' => 'multichoice_answers' ); - - for ($i = 0; $i < $node->rows; $i++) { + + $answers = $node->answers ? $node->answers : array(); + $answer = current($answers); + for ($i = 0; $i < $rows; $i++) { $form['answers'][$i]['correct'] = array( '#type' => 'checkbox', - '#default_value' => $answers[$i]['points'], + '#default_value' => isset($answer['points']) ? $answer['points'] : 0 ); - + $form['answers'][$i]['answer'] = array( '#type' => 'textarea', - '#default_value' => $answers[$i]['answer'], - '#cols' => 30, '#rows' => 2, + '#default_value' => isset($answer['answer']) ? $answer['answer'] : '' ); - + $form['answers'][$i]['feedback'] = array( '#type' => 'textarea', - '#default_value' => $answers[$i]['feedback'], - '#cols' => 30, '#rows' => 2, + '#default_value' => isset($answer['feedback']) ? $answer['feedback'] : '' ); - - if ($answers[$i]['aid']) { - $form['answers'][$i]['delete'] = array( - '#type' => 'checkbox', - '#default_value' => 0, - ); - $form['answers'][$i]['aid'] = array( - '#type' => 'hidden', - '#value' => $answers[$i]['aid'], - ); + if (isset($answer['aid'])) { + $form['answers'][$i]['delete'] = array('#type' => 'checkbox', '#default_value' => 0); + $form['answers'][$i]['aid'] = array('#type' => 'value', '#value' => $answer['aid']); } + $answer = next($answers); + } + + // It is up to the administrator to ensure that users with the 'create + // multiple-choice questions' permission have access to the default_format + // returned by quiz_variable_get. + $form['answers']['answers_format'] = filter_form(isset($node->answers_format) ? $node->answers_format : quiz_variable_get('default_format'), NULL, array('answers_format')); + // The format form is not displayed if only a single format is available, in + // which case it is passed as '#type' => 'value'. + if ($form['answers']['answers_format']['#type'] == 'fieldset') { + $form['answers']['answers_format']['#description'] = t('The chosen input format will be applied to all answer and feedback fields above.'); } - $form['more'] = array( + $form['answers']['multiple_answers'] = array( + '#type' => 'checkbox', + '#title' => t('This question has multiple correct answers.'), + '#description' => t('Check if this question has more than one correct answer or alternatively, if you would prefer to display a set of checkboxes instead of radio buttons even if there is only one correct answer.'), + '#default_value' => $node->multiple_answers, + '#parents' => array('multiple_answers') + ); + + $form['answers']['more'] = array( '#type' => 'checkbox', - '#title' => t('I need more answers'), + '#title' => t('I need more answer fields.'), + '#description' => t('Check and click on the preview button to add more answer fields to the form.'), + '#parents' => array('more') ); + $form['explanation'] = array( + '#type' => 'textarea', + '#title' => t('Explanation'), + '#description' => t('A detailed explanation of the answers. This field is usually displayed only when feedback is provided after each question. It is displayed regardless of the result.'), + '#default_value' => $node->explanation ? $node->explanation : '' + ); + $form['explanation_format'] = filter_form($node->explanation_format, NULL, array('explanation_format')); + return $form; } @@ -151,55 +168,29 @@ * Implementation of hook_validate(). */ function multichoice_validate(&$node) { + $answers = $corrects = 0; - // Hard-code questions to have no teaser and to not be promoted to front page - $node->teaser = 0; - $node->promote = 0; - - if (!$node->nid && empty($_POST)) return; - - // Validate body - if (!$node->body) { - form_set_error('body', t('Question text is empty')); - } - - // Validate answers - $answers = 0; - $corrects = 0; - - while (list($key, $answer) = each($node->answers)) { - if (trim($answer['answer']) != '') { - $answers++; - if ($answer['correct'] > 0) { - if ($corrects && !$node->multiple_answers) { - form_set_error('multiple_answers', t('Single choice yet multiple correct answers are present')); - } - $corrects++; + foreach ($node->answers as $key => $answer) { + if ($answer['correct']) { + if ($corrects && !$node->multiple_answers) { + form_set_error('multiple_answers', t('If this question has multiple correct answers, please enable the multiple correct answers checkbox.')); } + $corrects++; } - else { - // warn feedback is present without an answer - if (trim($answer['feedback']) != '') { - form_set_error("answers][$key][feedback", t('Feedback is given without an answer.')); - } - // warn marking correct without answer - if ($answer['correct'] > 0) { - form_set_error("answers][$key][correct", t('Empty answer marked as correct choice.')); - } + $answer['answer'] = trim($answer['answer']); + if (strlen($answer['answer'])) { + $answers++; + } + else if ($answer['correct'] || !empty($answer['feedback'])) { + form_set_error('answers][$key][answer', t('No answer entered for choice %d.', array('%d' => $key))); } } - if (!$corrects) { - form_set_error("answers][0]['correct'", t('No correct choice(s).')); - } - if ($node->multiple_answers && $corrects < 2) { - form_set_error('multiple_answers', t('Multiple answers selected, but only %count correct answer(s) present.', array('%count' => $corrects))); - } - if (!$answers) { - form_set_error("answers][0]['answer'", t('No answers.')); - } if ($answers < 2) { - form_set_error("answers][1]['answer'", t('Must have at least two answers.')); + form_set_error('answers][0][answer', t('A multiple-choice question requires a minimum of 2 answers.')); + } + if (!$corrects) { + form_set_error('answers][0][correct', t('No correct answer(s) selected.')); } } @@ -207,12 +198,13 @@ * Implementation of hook_insert(). */ function multichoice_insert(&$node) { - db_query("INSERT INTO {quiz_question} (nid, properties) VALUES(%d, '%s')", $node->nid, serialize(array('multiple_answers' => $node->multiple_answers))); - - while (list($key, $value) = each($node->answers)) { - if (trim($value['answer']) != "") - db_query("INSERT INTO {quiz_question_answer} (aid, question_nid, answer, feedback, points) VALUES(%d, %d, '%s', '%s', %d)", - db_next_id('{quiz_question_answer}_aid'), $node->nid, $value['answer'], $value['feedback'], $value['correct']); + db_query("INSERT INTO {quiz_question} (nid, vid, properties, answers_format, explanation, explanation_format) VALUES(%d, %d, '%s', %d, '%s', %d)", $node->nid, $node->vid, serialize(array('multiple_answers' => $node->multiple_answers)), $node->answers_format, $node->explanation, $node->explanation_format); + + foreach ($node->answers as $value) { + $answer = trim($value['answer']); + if (strlen($answer)) { + db_query("INSERT INTO {quiz_question_answer} (question_nid, question_vid, answer, feedback, points) VALUES(%d, %d, '%s', '%s', %d)", $node->nid, $node->vid, $value['answer'], $value['feedback'], $value['correct']); + } } } @@ -220,24 +212,34 @@ * Implementation of hook_update(). */ function multichoice_update($node) { - db_query("UPDATE {quiz_question} SET properties = '%s' WHERE nid = %d", serialize(array('multiple_answers' => $node->multiple_answers)), $node->nid); - - while (list($key, $value) = each($node->answers)) { - if ($value['aid']) { - $value['answer'] = trim($value['answer']); - if ($value['delete'] == 1 || empty($value['answer'])) { - //Delete this entry - db_query("DELETE FROM {quiz_question_answer} WHERE aid = %d", $value['aid']); - } - else { - //Update this entry - db_query("UPDATE {quiz_question_answer} SET answer = '%s', feedback = '%s', points = %s WHERE aid = %d", $value['answer'], $value['feedback'], $value['correct'], $value['aid']); + if ($node->revision) { + db_query("INSERT INTO {quiz_question} (nid, vid, properties, answers_format, explanation, explanation_format) VALUES(%d, %d, '%s', %d, '%s', %d)", $node->nid, $node->vid, serialize(array('multiple_answers' => $node->multiple_answers)), $node->answers_format, $node->explanation, $node->explanation_format); + foreach ($node->answers as $value) { + $answer = trim($value['answer']); + if (strlen($answer)) { + db_query("INSERT INTO {quiz_question_answer} (question_nid, question_vid, answer, feedback, points) VALUES(%d, %d, '%s', '%s', %d)", $node->nid, $node->vid, $value['answer'], $value['feedback'], $value['correct']); + } + } + } + else { + db_query("UPDATE {quiz_question} SET properties = '%s', answers_format = %d, explanation = '%s', explanation_format = %d WHERE vid = %d", serialize(array('multiple_answers' => $node->multiple_answers)), $node->answers_format, $node->explanation, $node->explanation_format, $node->vid); + + foreach ($node->answers as $key => $value) { + $answer = trim($value['answer']); + // Check if the answer already exists. + if ($value['aid']) { + if (!strlen($answer) || $value['delete'] == 1) { + db_query("DELETE FROM {quiz_question_answer} WHERE aid = %d", $value['aid']); + } + else { + db_query("UPDATE {quiz_question_answer} SET answer = '%s', feedback = '%s', points = %d WHERE aid = %d", $value['answer'], $value['feedback'], $value['correct'], $value['aid']); + } + } + else if (strlen($answer)) { + // If there is an answer, insert a new row. + db_query("INSERT INTO {quiz_question_answer} (question_nid, question_vid, answer, feedback, points) VALUES(%d, %d, '%s', '%s', %d)", + $node->nid, $node->vid, $value['answer'], $value['feedback'], $value['correct']); } - } - else if (trim($value['answer']) != "") { - //If there is an answer, insert a new row - db_query("INSERT INTO {quiz_question_answer} (aid, question_nid, answer, feedback, points) VALUES(%d, %d, '%s', '%s', %d)", - db_next_id('{quiz_question_answer}_aid'), $node->nid, $value['answer'], $value['feedback'], $value['correct']); } } } @@ -248,279 +250,333 @@ function multichoice_delete(&$node) { db_query("DELETE FROM {quiz_question_answer} WHERE question_nid = %d", $node->nid); db_query("DELETE FROM {quiz_question} WHERE nid = %d", $node->nid); + // FIXME: A score recalculation should be triggered or some other solution + // needs to be sought. + db_query("DELETE FROM {quiz_question_results} WHERE question_nid = %d", $node->nid); } /** * Implementation of hook_load(). */ function multichoice_load($node) { - $additions = db_fetch_object(db_query("SELECT * FROM {quiz_question} WHERE nid = %d", $node->nid)); + $additions = db_fetch_object(db_query("SELECT * FROM {quiz_question} WHERE vid = %d", $node->vid)); $answers = array(); - $result = db_query("SELECT * FROM {quiz_question_answer} WHERE question_nid = %d", $node->nid); + $result = db_query("SELECT * FROM {quiz_question_answer} WHERE question_vid = %d", $node->vid); while ($line = db_fetch_array($result)) { - $answers[] = $line; + $answers[$line['aid']] = $line; } $additions->answers = $answers; $additions->properties = unserialize($additions->properties); $additions->multiple_answers = $additions->properties['multiple_answers']; + return $additions; } /** * Implementation of hook_view(). */ -function multichoice_view(&$node, $teaser = FALSE, $page = FALSE) { - if (user_access('create multichoice')) { +function multichoice_view($node, $teaser = FALSE, $page = FALSE) { + if (user_access('create multiple-choice questions')) { if (!$teaser) { - $mynode = node_prepare($node, $teaser); - $mynode->content['body'] = array('#value' => multichoice_render_question($node)); - return $mynode; - //$node->body = multichoice_render_question($node); + $node = node_prepare($node, $teaser); + $node->content['body'] = array('#value' => multichoice_render_question($node, NULL, 'view')); + + return $node; } } - else if ($teaser) { - $mynode = node_prepare($node, $teaser); - return $mynode; - //$node->teaser = t('This is a quiz question, not to be viewed independently.'); - //$node->body = $node->teaser; // we do not need Read more... - } - else { - drupal_access_denied(); - } + + // FIXME: End users should not be able to access the question directly. That + // said, question moderation etc. should also be allowed. Perhaps an + // additional permission can be added, or alternatively, leave access control + // to an external module. + drupal_access_denied(); } /** - * Print question to screen + * Hook called by the quiz module's quiz engine to display the question form. * - * @param $node - * Question node + * @param $question + * Question node. + * @param $quiz_nid + * Node ID of the quiz node that the above question belongs to. + * @param $caller + * Is the caller node_view or the quiz engine? * * @return - * HTML output + * Rendered question form. */ -function multichoice_render_question_form($node) { - // Radio buttons for single selection questions, checkboxes for multiselect - if ($node->multiple_answers == 0) { +function multichoice_render_question($question, $quiz_nid = NULL, $caller = 'quiz') { + return drupal_get_form('multichoice_render_question_form', $question, $quiz_nid, $caller); +} + +/** + * Print question to screen. + * + * @param $question + * Question node. + * @param $quiz_nid + * Node ID of the quiz node that the above question belongs to. + * @param $caller + * Is the caller node_view or the quiz engine? + * + * @return + * Question form. + */ +function multichoice_render_question_form($question, $quiz_nid, $caller) { + // Radio buttons for single selection questions, checkboxes for multiselect. + if ($question->multiple_answers == 0) { $type = 'radios'; } else { $type = 'checkboxes'; } - // Get options - $options = array(); + $options = $tracker = array(); + + // Shuffle answer order. The aids are retaining in the value. + // TODO: Perhaps this should be a configuration option. + shuffle($question->answers); + + foreach ($question->answers as $key => $answer) { + // Increment $key by one to avoid the 0 index in the checkboxes field in + // light of the use of array_filter() in _submit. + $options[$key + 1] = check_markup($answer['answer'], $question->answers_format, FALSE); + // Keep track of aid <-> array index associations. This masking is done to + // ensure that the presence of aids in the page source cannot be taken + // advantage of by scripts. This also allows answers to be shuffled. + $tracker[$key + 1] = $answer['aid']; + } - while (list($key, $answer) = each($node->answers)) { - if (empty($answer['correct']) && !isset($answer['answer']) && empty($answer['feedback'])) { - unset($node->answers[$key]); - } - else { - $options[$key] = '
'. check_markup($answer['answer'], $node->format, FALSE) .'
'; - } - } - - $form['start'] = array('#type' => 'markup', '#value' => '
'); - $form['question'] = array('#type' => 'markup', '#value' => check_markup($node->body, $node->format, FALSE)); - - // Create form + $form['question'] = array('#value' => check_markup($question->body, $question->format, FALSE)); $form['tries'] = array( '#type' => $type, + '#title' => t('Options'), '#options' => $options, - '#default_value' => -1, + '#required' => TRUE ); - - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Submit'), - ); - + + // Display buttons only if the question is being called as part of a quiz. + if ($caller == 'quiz') { + // FIXME: Clean up implementation. + if (empty($_POST)) { + // Store tracker information in the session as storing it as: + // * a hidden field will accessible to end users. + // * a 'value' field will result in the existing data being overwritten + // on the submit page as value fields are regenerated along with the + // the form (and the corresponding call to shuffle()). + $_SESSION['quiz_'. $quiz_nid]['tracker'] = $tracker; + } + + $form['question_nid'] = array('#type' => 'value', '#value' => $question->nid); + $form['quiz_nid'] = array('#type' => 'value', '#value' => $quiz_nid); + + $form['submit'] = array('#type' => 'submit', '#value' => t('Submit')); + } + return $form; } -function multichoice_render_question($node) { - return drupal_get_form('multichoice_render_question_form', $node); +/** + * Process question form submissions. + */ +function multichoice_render_question_form_submit($form_id, $form_values) { + $session_id = 'quiz_'. $form_values['quiz_nid']; + $tracker = $_SESSION[$session_id]['tracker']; + unset($_SESSION[$session_id]['tracker']); + + $tries = is_array($form_values['tries']) ? array_filter($form_values['tries']) : $form_values['tries']; + $input = multichoice_structure_input($tries, $tracker); + + // Load question node (cached). + $question = node_load($form_values['question_nid']); + // FIXME: Avoid having to pass quiz_nid around like below. + quiz_update_result($form_values['quiz_nid'], $question, $input); } /** - * Evaluate whether question is correct + * Structure user input for storage in the questions_results table. This + * includes translating the IDs of user input to the internal aids stored in the + * tracker. * - * @param $nid - * Question Node ID + * @param $tries + * User input. + * @param $tracker + * An array that associates form answer fields with internal answer IDs. * * @return - * Array of results, in the form of: - * array( - * 'answers' => array of correct answer(s) - * 'tried' => array of selected answer(s) - * ); - */ -function multichoice_evaluate_question($nid) { - $question = node_load($nid); - $results = array(); - - if (isset($_POST['tries'])) { - if (is_array($_POST['tries'])) { - - // Multi-answer question - while (list($key, $try) = each($_POST['tries'])) { - $results['answers'] = $question->answers; - $results['tried'][] = $question->answers[$try]['aid']; - } - } - else { + * Array of user input. + */ +function multichoice_structure_input($tries, $tracker) { + $input = array(); - // Single-answer question - $results['answers'] = $question->answers; - $results['tried'][] = $question->answers[$_POST['tries']]['aid']; + if (is_array($tries)) { + // Multi-answer question. + foreach ($tries as $try) { + $input[] = $tracker[$try]; } } - //Unset $_POST, otherwise it tries to use the previous answers on the next page... - unset($_POST['tries']); - - //Return the result - return $results; + else { + // Single-answer question. + $input[] = $tracker[$tries]; + } + + return $input; } -//Old claculate result function +/** + * Hook called to calculate the result (score) of a question. + * + * @param $answers + * An array of correct answers. + * @param $tried + * An array of user input. + * + * @return + * Boolean: True if the answer is correct; false, if not. + */ function multichoice_calculate_result($answers, $tried) { - if (empty($answers)) return; - while (list($key, $answer) = each($answers)) { + foreach ($answers as $key => $answer) { if ($answer['points'] == 1) { if (($key = array_search($answer['aid'], $tried)) !== FALSE) { - //Correct answer was selected, so lets take that out the tried list + // Correct answer was selected, so let's take that out the tried list. unset($tried[$key]); } else { - //Correct answer was not in the "tried" list, so score 0 - return 0; + // Correct answer was not in the "tried" list, so score 0. + return FALSE; } } } - //Finally - have we got any answers left? - //If so - they weren't knocked out as one of the correct ones so logically they must be incorrect! - if (count($tried) > 0) return 0; - - - //Finally, we can consider this correct if its passed the above tests! - return 1; + // Finally - have we got any answers left? + // If so - they weren't knocked out as one of the correct ones so logically + // they must be incorrect! + if (count($tried) > 0) { + return FALSE; + } + + // Finally, we can consider this correct if it passed the above tests! + return TRUE; } -//New singing and dancing one -function multichoice_calculate_results($answers, $tried, $showpoints = FALSE, $showfeedback = FALSE) { - //Create results table +/** + * Hook called to calculate the result (score) of a question and return an HTML + * table tabulating correct answers and user input. + * + * @param $question + * The question node being processed. + * @param $tried + * An array of user input. + * @param $show_points + * Boolean controlling if the correct answer should be shown. + * @param $show_feedback + * Boolean controlling if the feedback fields should be displayed for each + * user input. + * + * @return + * An associative array with keys 'score' and 'results_table'. The 'score' + * field indicates if the user is correct or not. The 'results_table' contains + * the rendered results table tabulating each answer. + */ +function multichoice_calculate_results($question, $tried, $show_points = FALSE, $show_feedback = FALSE) { + $answers = $question->answers; + // Create results table. $rows = array(); - $correctanswers = array(); - - while (list($key, $answer) = each($answers)) { + $correct_answers = array(); + + foreach ($answers as $answer) { $cols = array(); - - $cols[] = $answer['answer']; - if ($showpoints) $cols[] = (($answer['points'] == 0) ? theme_multichoice_unselected() : theme_multichoice_selected()); - $selected = (array_search($answer['aid'], $tried) !== FALSE); - $cols[] = ($selected ? theme_multichoice_selected() : theme_multichoice_unselected()); - if ($showfeedback) $cols[] = ($selected ? '
'. $answer['feedback'] .'
' : ''); - + + $cols[] = check_markup($answer['answer'], $question->answers_format, FALSE); + if ($show_points) { + $cols[] = $answer['points'] == 0 ? theme_multichoice_unselected() : theme_multichoice_selected(); + } + $selected = array_search($answer['aid'], $tried) !== FALSE; + $cols[] = $selected ? theme_multichoice_selected() : theme_multichoice_unselected(); + if ($show_feedback) { + $cols[] = $selected ? '
'. check_markup($answer['feedback'], $question->answers_format, FALSE) .'
' : ''; + } + $rows[] = $cols; - - + if ($answer['points'] > 0) { - $correctanswers[] = $answer['aid']; + $correct_answers[] = $answer['aid']; } } - - if ($correctanswers === $tried) { - $score = 1; - } - else { - $score = 0; - } + // Since user input is shuffled, sort before comparing. + sort($correct_answers); + sort($tried); - return array('score' => $score, 'resultstable' => $rows); -} + $score = $correct_answers === $tried ? TRUE : FALSE; -/** - * List all multiple choice questions - * - * @return - * Array of questions - */ -function multichoice_list_questions() { - $result = db_query("SELECT nid, body, format FROM {node} WHERE type= '%s'", 'multichoice'); - $questions = array(); - while ($node = db_fetch_object($result)) { - $question = new stdClass(); - $question->question = check_markup($node->body, $node->format); - $question->nid = $node->nid; - $questions[] = $question; - } - return $questions; + return array('score' => $score, 'results_table' => $rows); } ///////////////////////////////////////////////// -/// Theme functions +// Theme functions ///////////////////////////////////////////////// /** - * Theme function for multichoice form + * Theme function for multichoice form. * - * Lays out answer field elements into a table + * Lays out answer field elements into a table. * * @return string - * HTML output + * HTML output. */ -function theme_multichoice_form($form) { - - // Format table header +function theme_multichoice_answers($form) { + // Format table header. $header = array( - array('data' => t('Correct')), - array('data' => t('Answer'), 'style' => 'width:250px;'), - array('data' => t('Feedback'), 'style' => 'width:250px;'), - array('data' => t('Delete')), + t('Correct'), + t('Answer'), + t('Feedback'), + t('Delete') ); - // Format table rows + // Format table rows. $rows = array(); foreach (element_children($form) as $key) { - $rows[] = array( - drupal_render($form[$key]['correct']), - drupal_render($form[$key]['answer']), - drupal_render($form[$key]['feedback']), - drupal_render($form[$key]['delete']), - ); + if (is_numeric($key)) { + $rows[] = array( + drupal_render($form[$key]['correct']), + drupal_render($form[$key]['answer']), + drupal_render($form[$key]['feedback']), + drupal_render($form[$key]['delete']) + ); + } } - // Theme output and display to screen - $output = theme('table', $header, $rows); + // Theme output and display to screen. + $output = theme('table', $header, $rows, array('class' => 'multichoice-choices-table')); + $output .= drupal_render($form['answers_format']); + $output .= drupal_render($form['multiple_answers']); + $output .= drupal_render($form['more']); return $output; } - /** - * Theme a selection indicator for an answer - * TODO: Default images would be nice + * Theme a selection indicator for an answer. + * + * @todo Default images would be nice. */ function theme_multichoice_selected() { return theme_image(drupal_get_path('module', 'quiz') .'/images/selected.gif', t('selected')); } - /** - * Theme an indicator that an answer is not selected / correct - * TODO: Default images would be nice + * Theme an indicator that an answer is not selected / correct. + * + * @todo Default images would be nice. */ function theme_multichoice_unselected() { return theme_image(drupal_get_path('module', 'quiz') .'/images/unselected.gif', t('unselected')); } - /** - * Theme function for the multichoice form + * Theme function for the multichoice form. */ function theme_multichoice_render_question_form($form) { - $output = ''; - $output .= drupal_render($form) .'
'; - return $output; + return '
'. drupal_render($form) .'
'; } Index: quiz.info =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/quiz/quiz.info,v retrieving revision 1.3.2.2 diff -u -r1.3.2.2 quiz.info --- quiz.info 18 Jun 2007 23:06:59 -0000 1.3.2.2 +++ quiz.info 21 Jun 2007 09:11:00 -0000 @@ -1,4 +1,5 @@ -; $Id: quiz.info,v 1.3.2.2 2007/06/18 23:06:59 dww Exp $ +; $Id: quiz.info 231 2007-06-20 07:46:11Z karthik $ name = Quiz package = Quiz -description = Create interactive quizzes +description = Create interactive quizzes. + Index: quiz.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/quiz/quiz.module,v retrieving revision 1.86.2.8 diff -u -r1.86.2.8 quiz.module --- quiz.module 20 Jun 2007 14:55:22 -0000 1.86.2.8 +++ quiz.module 21 Jun 2007 09:11:00 -0000 @@ -1,166 +1,252 @@ uid == $node->uid)) { - return TRUE; - } - } - - if (user_access('administer quiz')) { - return TRUE; +function quiz_help($section) { + switch ($section) { + case 'admin/help#quiz': + return t(' +

Description

+

The quiz module allows users to administer a quiz, as a sequence of questions, and track the answers given. It allows for the creation of questions (and their answers), and organises these questions into a quiz. Finally, it provides a mechanism for ensuring question quality through a combination of community revision and moderation. Its target audience includes educational institutions, online training programs, employers, and people who just want to add a fun activity for their visitors to their Drupal site.

+

Creating Your First Quiz

+

Creating an initial quiz requires three steps:

+
    +
  1. Create at least one taxonomy vocabulary and assign it to the quiz and question type modules
  2. +
  3. Create a series of questions
  4. +
  5. Create a quiz based on the series of questions
  6. +
+

Also note that for anyone but the site administrator, creating quizzes requires the create quiz privilege, and creating questions requires the administer question type privilege. These settings can be configured in Administer >> User management >> Access control.

+

Setting up a vocabulary

+
    +
  1. If not already enabled, go to the Administer >> Site building >> Modules section of the control panel and check the enable checkbox to enable the taxonomy module.
  2. +
  3. If you do not already have a taxonomy vocabulary suitable for quizzes, go to Administer >> Content management >> Categories and create a vocabulary for quizzes (for example, Quiz Topics). Ensure that under Types, both quiz and all question types (for example, multichoice) are selected. Depending on your needs, you may wish to create a hierarchical vocabulary, so that topics can be sub-divided into smaller areas, and/or enable multiple select to associate quizzes and questions with more than one category.
  4. +
  5. Add a series of terms to the vocabulary to which questions and quizzes can be assigned. For example: +
+

Creating quiz questions

+
    +
  1. Begin by clicking Create content, and then select a question type node (for example, multichoice)
  2. +
  3. Fill out the question form. The presented interface will vary depending on the question type, but for multiple-choice questions: +
    +
    Title
    +
    Question title. This will be displayed as the heading of the question.
    +
    Taxonomy selection
    +
    Any taxonomy vocabularies that are assigned to the question type will be displayed. Select from the terms displayed in order to assign questions to vocabulary terms so that they can be filtered in the quiz creation process.
    +
    Question
    +
    The actual question text (for example, What is 2+2?).
    +
    Multiple Answers
    +
    Whether or not the question has multiple correct answers, such as a "Select all that apply" question.
    +
    Correct
    +
    Indicates that given answer is a correct answer.
    +
    Answer
    +
    An answer choice (for example, 4). If more answers are required, check I need more answers and click the Preview button.
    +
    Feedback
    +
    Feedback, if supplied, will be provided to the user either immediately after answering the question, at the end of the quiz, or not at all (feedback options are configured in quizzes).
    +
    +
  4. +
  5. Repeat for each question you would like included on the quiz.
  6. +
+

Creating the quiz

+
    +
  1. Go to Create content >> Quiz to access the quiz creation form.
  2. +
  3. Fill out the form to set the @quiz options: +
    +
    Title
    +
    Quiz title. This will be displayed as the heading of the quiz.
    +
    Taxonomy selection
    +
    Any taxonomy vocabularies that are assigned to the quiz type will be displayed. Select from the terms displayed in order to assign the quiz to vocabulary terms.
    +
    Number of questions
    +
    Total number of questions on quiz.
    +
    Shuffle questions
    +
    Whether or not to shuffle (randomise) the questions.
    +
    Feedback
    +
    Set options on when and whether question feedback should appear.
    +
    Number of takes
    +
    Number of takes to allow user. Varies from 1-9 or Unlimited times.
    +
    +
  4. +
  5. Once the quiz has been created, click the add questions tab to assign questions to the quiz.
  6. +
  7. Begin by selecting one or more terms from the taxonomy list and click Filter question list. This will bring up a selection of questions matching the selected vocabulary terms.
  8. +
  9. Select a radio button next to each question indicating if the question should appear (Randomly, Always, or Never), and click Submit questions.
  10. +
  11. Repeat process until satisfied with question selection.
  12. +
+ ', array('@quiz' => QUIZ_NAME, '@admin-access' => url('admin/user/access'), '@admin-modules' => url('admin/build/modules'), '@admin-taxonomy' => url('admin/content/taxonomy'), '@create-content' => url('node/add'), '@multichoice' => url('node/add/multichoice'), '@create-quiz' => url('node/add/quiz'))); + case 'node/add#quiz': + return t('A collection of questions designed to create interactive tests'); + default: + break; } } /** - * Implementation of hook_node_info(). - */ -function quiz_node_info() { - return array( - 'quiz' => array( - 'name' => t('@quiz', array("@quiz" => QUIZ_NAME)), - 'module' => 'quiz', - 'description' => 'Create interactive quizzes for site visitors')); -} - -/** * Implementation of hook_menu(). */ function quiz_menu($may_cache) { $items = array(); if ($may_cache) { - $access = user_access(QUIZ_PERM_ADMIN_CONFIG); $items[] = array( 'path' => 'admin/settings/quiz', - 'title' => t('@quiz Configuration', array('@quiz' => QUIZ_NAME)), - 'description' => t('Describes what the settings generally do.'), + 'title' => t('Quiz'), + 'description' => t('Configure settings for the quiz module.'), 'callback' => 'drupal_get_form', - 'callback arguments' => array('quiz_admin_settings'), - 'access' => user_access(QUIZ_PERM_ADMIN_CONFIG), - 'type' => MENU_NORMAL_ITEM, // optional + 'callback arguments' => array('quiz_admin_settings_form'), + 'access' => user_access('administer quiz configuration'), + 'type' => MENU_NORMAL_ITEM + ); + + $items[] = array( + 'path' => 'admin/quiz', + 'title' => t('@quiz results', array('@quiz' => QUIZ_NAME)), + 'callback' => 'quiz_admin', + 'access' => user_access('administer quiz'), + 'type' => MENU_NORMAL_ITEM ); $items[] = array( 'path' => 'node/add/quiz', 'title' => t('@quiz', array('@quiz' => QUIZ_NAME)), - 'access' => user_access('create quiz'), + 'access' => user_access('create quiz') ); - - $items[] = array( - 'path' => 'admin/quiz', - 'title' => t('@quiz Results', array('@quiz' => QUIZ_NAME)), - 'callback' => 'quiz_admin', - 'access' => user_access('administer quiz'), - 'type' => MENU_NORMAL_ITEM, - ); } else { - drupal_add_css(drupal_get_path('module', 'quiz') .'/quiz.css', 'module', 'all'); - if (arg(0) == 'node' && is_numeric(arg(1))) { - $node = node_load(arg(1)); - if ($node->type == 'quiz') { - - // Menu item for creating adding questions to quiz - $items[] = array( - 'path' => 'node/'. arg(1) .'/questions', - 'title' => t('Manage questions'), - 'callback' => 'quiz_questions', - 'access' => user_access('create quiz'), - 'type' => MENU_LOCAL_TASK, - ); - - // Menu item for quiz taking interface - $items[] = array( - 'path' => 'node/'. arg(1) .'/quiz/start', - 'title' => t('Take @quiz', array('@quiz' => QUIZ_NAME)), - 'callback' => 'quiz_take_quiz', - 'access' => user_access('access quiz'), - 'type' => MENU_LOCAL_TASK, - ); + $path = drupal_get_path('module', 'quiz'); + include($path .'/quiz_datetime.inc'); + drupal_add_css($path .'/quiz.css'); + + if (arg(0) == 'node' && is_numeric(arg(1))) { + $node = node_load(arg(1)); + if ($node->type == 'quiz') { + // Menu item for assigning and unassigning questions to the quiz. + $items[] = array( + 'path' => 'node/'. arg(1) .'/questions', + 'title' => t('Manage questions'), + 'callback' => 'quiz_manage_questions', + 'access' => user_access('create quiz'), + 'type' => MENU_LOCAL_TASK + ); + + // Menu item for quiz taking interface. + $items[] = array( + 'path' => 'node/'. arg(1) .'/engine', + 'title' => t('Take @quiz', array('@quiz' => QUIZ_NAME)), + 'callback' => 'quiz_engine', + 'access' => user_access('access quiz'), + 'type' => MENU_CALLBACK + ); } } else { $items[] = array( - 'path' => 'user/'. arg(1) .'/myresults', - 'title' => t('my results'), - 'callback' => 'quiz_get_user_results', - 'access' => user_access('user results'), - 'type' => MENU_LOCAL_TASK, - ); - - $items[] = array( - 'path' => 'user/quiz/'. arg(2) .'/userresults', - 'title' => t('User Results'), - 'callback' => 'quiz_user_results', - 'access' => user_access('user results'), - 'type' => MENU_CALLBACK, - ); - - $items[] = array( - 'path' => 'admin/quiz/'. arg(2) .'/view', + 'path' => 'admin/quiz/'. arg(2), 'title' => t('View @quiz', array('@quiz' => QUIZ_NAME)), 'callback' => 'quiz_admin_results', 'access' => user_access('administer quiz'), - 'type' => MENU_CALLBACK, + 'type' => MENU_CALLBACK ); - + $items[] = array( 'path' => 'admin/quiz/'. arg(2) .'/delete', 'title' => t('Delete @quiz', array('@quiz' => QUIZ_NAME)), - 'callback' => 'quiz_admin_result_delete', + 'callback' => 'drupal_get_form', + 'callback arguments' => 'quiz_admin_result_delete_form', 'access' => user_access('administer quiz'), - 'type' => MENU_CALLBACK); + 'type' => MENU_CALLBACK + ); + + $items[] = array( + 'path' => 'user/'. arg(1) .'/myresults', + 'title' => t('My results'), + 'callback' => 'quiz_get_user_results', + 'access' => user_access('access quiz results'), + 'type' => MENU_LOCAL_TASK + ); + $items[] = array( + 'path' => 'user/quiz/'. arg(2), + 'title' => t('User results'), + 'callback' => 'quiz_user_results', + 'access' => user_access('access quiz results'), + 'type' => MENU_CALLBACK + ); } } @@ -168,233 +254,182 @@ } /** + * Implementation of hook_perm(). + */ +function quiz_perm() { + return array('administer quiz configuration', 'administer quiz', 'create quiz', 'access quiz', 'access quiz results'); +} + +/** + * Implementation of hook_access(). + */ +function quiz_access($op, $node) { + global $user; + + if ($op == 'view') { + return user_access('access quiz'); + } + + if ($op == 'create') { + return user_access('create quiz'); + } + + if ($op == 'update' || $op == 'delete') { + if (user_access('create quiz') && ($user->uid == $node->uid)) { + return TRUE; + } + } + + if (user_access('administer quiz')) { + return TRUE; + } +} + +/** + * Implementation of hook_node_info(). + */ +function quiz_node_info() { + return array( + 'quiz' => array( + 'name' => t('@quiz', array("@quiz" => QUIZ_NAME)), + 'module' => 'quiz', + 'description' => t('Create an interactive @quiz for site visitors.', array('@quiz' => QUIZ_NAME)) + ) + ); +} + +/** * Implementation of hook_form(). */ -function quiz_form(&$node) { +function quiz_form(&$node) { $form['title'] = array( '#type' => 'textfield', '#title' => t('Title'), '#default_value' => $node->title, - '#description' => t('The name of the @quiz', array('@quiz' => QUIZ_NAME)), - '#required' => TRUE, + '#description' => t('The name of the @quiz.', array('@quiz' => QUIZ_NAME)), + '#required' => TRUE ); $form['body'] = array( '#type' => 'textarea', '#title' => t('Description'), '#default_value' => $node->body, - '#description' => t('A description of what the @quiz entails', array('@quiz' => QUIZ_NAME)), - '#required' => TRUE, + '#description' => t('A description of the @quiz.', array('@quiz' => QUIZ_NAME)), + '#required' => TRUE ); $form['body_filter']['format'] = filter_form($node->format); - $form['number_of_questions'] = array( + $form['properties'] = array( + '#type' => 'fieldset', + '#title' => t('Properties'), + '#collapsible' => TRUE, + '#collapsed' => FALSE + ); + $form['properties']['number_of_questions'] = array( '#type' => 'textfield', - '#title' => t('Number of questions'), - '#default_value' => ($node->number_of_questions ? $node->number_of_questions : 10), - '#description' => t('The number of questions to include in this @quiz from the question bank', array('@quiz' => QUIZ_NAME)), - '#required' => TRUE, + '#title' => t('Maximum number of questions'), + '#default_value' => $node->number_of_questions ? $node->number_of_questions : 0, + '#description' => t('The maximum number of questions to include in this @quiz from the question bank. Enter 0 if you would like all questions to be included.', array('@quiz' => QUIZ_NAME)), + '#required' => TRUE + ); + $form['properties']['pass_rate'] = array( + '#type' => 'select', + '#title' => t('Pass percentage', array('@quiz' => QUIZ_NAME)), + '#options' => range(0, 100), + '#default_value' => $node->pass_rate ? $node->pass_rate : quiz_variable_get('pass_mark'), + '#description' => t('Percentage required to pass the @quiz. Set to 0 if not required.', array('@quiz' => QUIZ_NAME)), + '#required' => FALSE ); - - $form['shuffle'] = array( + $form['properties']['shuffle'] = array( '#type' => 'checkbox', - '#title' => t('Shuffle questions'), - '#default_value' => (isset($node->shuffle) ? $node->shuffle : 1), - '#description' => t('Whether to shuffle/randomize the questions on the @quiz', array('@quiz' => QUIZ_NAME)), + '#title' => t('Shuffle questions.'), + '#default_value' => isset($node->shuffle) ? $node->shuffle : 1, + '#description' => t('Whether to shuffle/randomise the questions on the @quiz.', array('@quiz' => QUIZ_NAME)) + ); + + $form['feedback'] = array( + '#type' => 'fieldset', + '#title' => t('Feedback'), + '#description' => t('When should feedback be given to the candidate?'), + '#collapsible' => TRUE, + '#collapsed' => TRUE + ); + $form['feedback']['feedback_time'] = array( + '#type' => 'radios', + '#default_value' => $node->feedback_time ? $node->feedback_time : FEEDBACK_AFTER_BOTH, + '#title' => t('Time of feedback'), + '#description' => t('Control when feedback on each question is displayed to the user.'), + '#options' => array( + FEEDBACK_AFTER_QUIZ => t('At the end of the @quiz.', array('@quiz' => QUIZ_NAME)), + FEEDBACK_AFTER_QUESTION => t('After each question.'), + FEEDBACK_AFTER_BOTH => t('After each question and at the end of the @quiz.', array('@quiz' => QUIZ_NAME)), + FEEDBACK_NEVER_SHOW => t('Never show.') + ), + '#required' => TRUE + ); + // TODO: Summaries should have their own input format. + $form['feedback']['summary_pass'] = array( + '#type' => 'textarea', + '#title' => t('Summary text if passed.'), + '#default_value' => $node->summary_pass, + '#description' => t('Summary to display when the user has passed the @quiz.', array('@quiz' => QUIZ_NAME)) + ); + $form['feedback']['summary_default'] = array( + '#type' => 'textarea', + '#title' => t('Default summary text.'), + '#default_value' => $node->summary_default, + '#description' => t("Summary to display by default. This is displayed if the user has not passed the quiz or if the required passing score has been set to 0 with the passing summary textfield left blank.") ); - // Set up the availability options + // Set up the availability options. $form['quiz_availability'] = array( '#type' => 'fieldset', '#title' => t('Availability options'), - '#collapsed' => FALSE, - '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#collapsible' => TRUE ); $form['quiz_availability']['quiz_always'] = array( '#type' => 'checkbox', - '#title' => t('Always Available'), - '#default_value' => $node->quiz_always, - '#description' => t('Click this option to ignore the open and close dates.'), + '#title' => t('Always available'), + '#default_value' => isset($node->quiz_always) ? $node->quiz_always : 1, + '#description' => t('If checked, the open and close dates will be ignored.') ); $form['quiz_availability']['quiz_open'] = array( '#type' => 'date', - '#title' => t('Open Date'), + '#title' => t('Open date'), '#default_value' => _quiz_form_prepare_date($node->quiz_open), - '#description' => t('The date this @quiz will become available.', array('@quiz' => QUIZ_NAME)), + '#description' => t('The date this @quiz will become available.', array('@quiz' => QUIZ_NAME)) ); $form['quiz_availability']['quiz_close'] = array( '#type' => 'date', - '#title' => t('Close Date'), - '#default_value' => _quiz_form_prepare_date($node->quiz_close, variable_get('quiz_default_close', 30)), - '#description' => t('The date this @quiz will cease to be available.', array('@quiz' => QUIZ_NAME)), + '#title' => t('Close date'), + '#default_value' => _quiz_form_prepare_date($node->quiz_close, quiz_variable_get('expire')), + '#description' => t('The date this @quiz will cease to be available.', array('@quiz' => QUIZ_NAME)) ); - - $options = array('Unlimited'); - for ($i = 1; $i < 10; $i++) { - $options[$i] = $i; - } - $form['takes'] = array( + $form['quiz_availability']['takes'] = array( '#type' => 'select', - '#title' => t('Number of takes'), + '#title' => t('Maximum number of takes'), '#default_value' => $node->takes, - '#options' => $options, - '#description' => t('The number of times a user is allowed to take the @quiz', array('@quiz' => QUIZ_NAME)), - ); - - // Quiz summary options - $form['summaryoptions'] = array( - '#type' => 'fieldset', - '#title' => t('@quiz Summary Options', array('@quiz' => QUIZ_NAME)), - '#collapsible' => TRUE, - '#collapsed' => FALSE, - ); - if (variable_get('quiz_use_passfail', 1)) { - // New nodes get the default. Otherwise they get 0. (ignore pass / fail feedback) - if (!$node->nid) { - $node->pass_rate = variable_get('quiz_default_pass_rate', 75); - } - $form['summaryoptions']['pass_rate'] = array( - '#type' => 'textfield', - '#title' => t('Pass rate for @quiz (%)', array('@quiz' => QUIZ_NAME)), - '#default_value' => ($node->pass_rate ? $node->pass_rate : 0), - '#description' => t('Pass rate for the @quiz as a percentage score', array('@quiz' => QUIZ_NAME)), - '#required' => FALSE, - ); - $form['summaryoptions']['summary_pass'] = array( - '#type' => 'textarea', - '#title' => t('Summary text if passed.'), - '#default_value' => $node->summary_pass, - '#cols' => 60, - '#description' => t('Summary for when the user gets enough correct answers to pass the @quiz. Leave blank if you don\'t want to give different summary text if they passed or if you are not using the "percent to pass" option above. If you don\'t use the "Percentage needed to pass" field above, this text will not be used.', array('@quiz' => QUIZ_NAME)), - ); - } - $form['summaryoptions']['summary_default'] = array( - '#type' => 'textarea', - '#title' => t('Default summary text.'), - '#default_value' => $node->summary_default, - '#cols' => 60, - '#description' => t("Default summary. Leave blank if you don't want to give a summary."), + '#options' => range(0, 100), + '#description' => t('The maximum number of times a user is allowed to take the @quiz. 0 represents an unlimited number of takes.', array('@quiz' => QUIZ_NAME)) ); return $form; } /** - * Takes a time element and prepares to send it to form_date() - * - * @param $time - * The time to be turned into an array. This can be: - * - a timestamp when from the database - * - an array (day, month, year) when previewing - * - null for new nodes - * @return - * an array for form_date (day, month, year) - */ -function _quiz_form_prepare_date($time = '', $offset = 0) { - // if this is empty, get the current time - if ($time == '') { - $time = time(); - $time = strtotime("+$offset days", $time); - } - // If we are previewing, $time will be an array so just pass it through - $time_array = array(); - if (is_array($time)) { - $time_array = $time; - } - // otherwise build the array from the timestamp - elseif (is_numeric($time)) { - $time_array = array( - 'day' => _quiz_date('j', $time), - 'month' => _quiz_date('n', $time), - 'year' => _quiz_date('Y', $time), - ); - } - // return the array - return $time_array; -} - -/** - * @param $nid - * Quiz ID - * Finds out the number of questions for the quiz. - * Good example of usage could be to calculate the % of score - * @return integer - * Returns the number of quiz questions. - */ -function quiz_get_number_of_questions($nid) { - $result = db_fetch_object(db_query('SELECT number_of_questions FROM {quiz} WHERE nid = %d', $nid)); - $number_of_questions = $result->number_of_questions; - return $number_of_questions; -} - -/** - * @param $nid - * Quiz ID - * Finds out the pass rate for the quiz. - * @return integer - * Returns the pass rate of quiz. - */ -function quiz_get_pass_rate($nid) { - $passrate = db_fetch_object(db_query('SELECT pass_rate FROM {quiz} WHERE nid = %d', $nid)); - return $passrate->pass_rate; -} - -/** * Implementation of hook_validate(). */ function quiz_validate($node) { - if (!$node->nid && empty($_POST)) return; - - if (empty($node->body)) { - form_set_error('body', t('Description is required.')); - } - - // validate the number of questions against the actual questions assigned to this quiz - if ($node->number_of_questions < 1) { - form_set_error('number_of_questions', t('Number of questions is required and must be a positive number.')); - } - else if ($node->nid) { - // get the number of each kind of question - $anum_random = quiz_get_num_questions($node->nid, QUESTION_RANDOM); - $anum_always = quiz_get_num_questions($node->nid, QUESTION_ALWAYS); - $anum_total = $anum_always + $anum_random; - // If we have random number, add one to the low range. - if ($anum_random > 0) { - $anum_always++; - } - // If we have more than one random number, lower the high range by one. - if ($anum_random > 1) { - $anum_total--; - } - // format the valid range - if ($anum_always != $anum_total) { - $range = t('between %low and %high', array('%low' => $anum_always, '%high' => $anum_total)); - } - else { - $range = $anum_total; - } - // If there are not enough questions to support this number. - if ($anum_total < $node->number_of_questions) { - form_set_error('number_of_questions', t("You don't currently have enough questions assigned to this @quiz to support that many questions. Either change the number of questions to %range or !action to this @quiz.", array('@quiz' => QUIZ_NAME, '%range' => $range, '!action' => l(t('add more questions'), 'node/'. $node->nid .'/questions')))); - } - // If there are too many questions for this number. - else if ($anum_always > $node->number_of_questions) { - form_set_error('number_of_questions', t('There are too many questions assigned to this @quiz to support that low of a number. Either change the number of questions to %range or !action from this @quiz.', array('@quiz' => QUIZ_NAME, '%range' => $range, '!action' => l(t('remove some questions'), 'node/'. $node->nid .'/questions')))); - } + // Validate the number of questions against the actual questions assigned to + // this quiz. + if (!is_numeric($node->number_of_questions) || $node->number_of_questions < 0) { + form_set_error('number_of_questions', t('The maximum number of questions must be a positive number.')); } if (mktime(0, 0, 0, $node->quiz_open['month'], $node->quiz_open['day'], $node->quiz_open['year']) > mktime(0, 0, 0, $node->quiz_close['month'], $node->quiz_close['day'], $node->quiz_close['year'])) { form_set_error('quiz_close', t('Please make sure the close date is after the open date.')); } - if (!is_numeric($node->pass_rate)) { - form_set_error('pass_rate', t('The pass rate value must be a number between 0% and 100%.')); - } - if ($node->pass_rate > 100) { - form_set_error('pass_rate', t('The pass rate value must not be more than 100%.')); - } - if ($node->pass_rate < 0) { - form_set_error('pass_rate', t('The pass rate value must not be less than 0%.')); - } } /** @@ -403,9 +438,9 @@ function quiz_insert($node) { quiz_translate_form_date($node, 'quiz_open'); quiz_translate_form_date($node, 'quiz_close'); - $sql = "INSERT INTO {quiz} (nid, number_of_questions, shuffle, quiz_open, quiz_close, takes, pass_rate, summary_pass, summary_default, quiz_always)"; - $sql .= " VALUES(%d, %d, %d, %d, %d, %d, %d, '%s', '%s', %d)"; - db_query($sql, $node->nid, $node->number_of_questions, $node->shuffle, $node->quiz_open, $node->quiz_close, $node->takes, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always); + $sql = "INSERT INTO {quiz} (nid, vid, number_of_questions, shuffle, feedback_time, quiz_open, quiz_close, takes, pass_rate, summary_pass, summary_default, quiz_always) + VALUES(%d, %d, %d, %d, %d, %d, %d, %d, %d, '%s', '%s', %d)"; + db_query($sql, $node->nid, $node->vid, $node->number_of_questions, $node->shuffle, $node->feedback_time, $node->quiz_open, $node->quiz_close, $node->takes, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always); } /** @@ -414,7 +449,23 @@ function quiz_update($node) { quiz_translate_form_date($node, 'quiz_open'); quiz_translate_form_date($node, 'quiz_close'); - db_query("UPDATE {quiz} SET number_of_questions = %d, shuffle = %d, quiz_open = %d, quiz_close = %d, takes = %d, pass_rate = %d, summary_pass = '%s', summary_default ='%s', quiz_always = %d WHERE nid = %d", $node->number_of_questions, $node->shuffle, $node->quiz_open, $node->quiz_close, $node->takes, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always, $node->nid); + + // Ensure that the number of questions accomodates all questions that have + // been set to 'always ask'. + $min_questions = quiz_validate_max_questions($node); + if ($min_questions !== TRUE) { + $node->number_of_questions = $min_questions; + drupal_set_message(t('The maximum number of questions for this @quiz has been increased to %min_questions to match the number of questions set to be asked always.', array('@quiz' => QUIZ_NAME, '%min_questions' => $min_questions))); + } + + if ($node->revision) { + $sql = "INSERT INTO {quiz} (nid, vid, number_of_questions, shuffle, feedback_time, quiz_open, quiz_close, takes, pass_rate, summary_pass, summary_default, quiz_always) + VALUES(%d, %d, %d, %d, %d, %d, %d, %d, %d, '%s', '%s', %d)"; + db_query($sql, $node->nid, $node->vid, $node->number_of_questions, $node->shuffle, $node->feedback_time, $node->quiz_open, $node->quiz_close, $node->takes, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always); + } + else { + db_query("UPDATE {quiz} SET number_of_questions = %d, shuffle = %d, feedback_time = %d, quiz_open = %d, quiz_close = %d, takes = %d, pass_rate = %d, summary_pass = '%s', summary_default ='%s', quiz_always = %d WHERE vid = %d", $node->number_of_questions, $node->shuffle, $node->feedback_time, $node->quiz_open, $node->quiz_close, $node->takes, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always, $node->vid); + } } /** @@ -423,452 +474,793 @@ function quiz_delete($node) { db_query('DELETE FROM {quiz} WHERE nid = %d', $node->nid); db_query('DELETE FROM {quiz_questions} WHERE quiz_nid = %d', $node->nid); + quiz_results_delete($node->nid); } /** * Implementation of hook_load(). */ function quiz_load($node) { - $additions = db_fetch_object(db_query('SELECT * FROM {quiz} WHERE nid = %d', $node->nid)); - $results = db_query('SELECT * FROM {quiz_questions} WHERE quiz_nid = %d', $node->nid); + $additions = db_fetch_object(db_query('SELECT * FROM {quiz} WHERE vid = %d', $node->vid)); + + // FIXME: Loading the entire question bank every time is folly. + $results = db_query('SELECT question_nid, question_priority FROM {quiz_questions} WHERE quiz_nid = %d', $node->nid); while ($question = db_fetch_object($results)) { - $additions->question_status[$question->question_nid] = $question->question_status; + $additions->question_priority[$question->question_nid] = $question->question_priority; } + + $additions->max_questions = $additions->number_of_questions == 0 ? count($additions->question_priority) : $additions->number_of_questions; + // If the number of questions allowed is greater than the number of questions + // available, choose the latter. + $additions->max_questions = min($additions->max_questions, count($additions->question_priority)); + return $additions; } /** * Implementation of hook_view(). */ - function quiz_view($node, $teaser = FALSE, $page = FALSE) { - if (!$teaser) { + $quiz_available = quiz_validate_availability($node); + if ($quiz_available === TRUE) { $node = node_prepare($node, $teaser); - if (user_access('create quiz') || user_access('administer quiz') || user_access(QUIZ_PERM_ADMIN_CONFIG)) { - $theme = 'quiz_view'; - } - else { - $theme = 'quiz_availability'; - } - $node->content['body']['#value'] .= theme($theme , $node, $teaser, $page); + $node->content['properties'] = array( + '#value' => theme('quiz_view', $node, $teaser, $page), + '#weight' => 2 + ); } + else { + // Display quiz unavailable message. + $node->content['body']['#value'] = $quiz_available; + } + return $node; } /** - * Themes a message about the quiz's availability for quiz takers - * - * + * Implementation of hook_link(). */ -function theme_quiz_availability($node) { - $output = '

'; - if (!$node->quiz_always) { - if ($node->quiz_open > time()) { - $output .= t('This quiz will not be available until %time.', array('%time' => format_date($node->quiz_open))); +function quiz_link($type, $node = NULL, $teaser = FALSE) { + $links = array(); + + // Check if this is a quiz node and if it is available. + if ($type == 'node' && $node->type == 'quiz' && quiz_validate_availability($node) === TRUE) { + $links['quiz_start'] = array( + 'title' => t('Begin @quiz', array('@quiz' => QUIZ_NAME)), + 'href' => "node/$node->nid/engine", + 'attributes' => array('title' => t('Begin lesson.')) + ); + } + + return $links; +} + +/** + * Menu callback: Quiz configuration form. + * + * @todo Spin off defaults into a separate tab along with access control + * settings for quiz properties. + */ +function quiz_admin_settings_form() { + $form = array('quiz' => array('#tree' => TRUE)); + $form['quiz']['general'] = array( + '#type' => 'fieldset', + '#title' => t('General settings'), + '#collapsible' => TRUE, + '#parents' => array('quiz') + ); + $form['quiz']['general']['name'] = array( + '#type' => 'textfield', + '#title' => t('Name'), + '#default_value' => QUIZ_NAME, + '#description' => t('The term used to refer to quizzes across the site (for example: quiz, test, assessment). This will affect display text but will not affect menu paths.'), + '#required' => TRUE + ); + $form['quiz']['general']['pager'] = array( + '#type' => 'select', + '#title' => t('Pager'), + '#options' => array( + 10 => 10, + 25 => 25, + 50 => 50, + 100 => 100 + ), + '#default_value' => quiz_variable_get('pager'), + '#description' => t('Number of entries to display per page on various pages belonging to the quiz module.', array('@quiz' => QUIZ_NAME)), + '#required' => TRUE + ); + + // Only allow users with administer filters access to modify the default + // format. This is to ensure that there are no issues when users with + // conflicting filter permissions modify the quiz configuration form. + if (user_access('administer filters')) { + $formats = filter_formats(); + $options = array(); + foreach ($formats as $id => $format) { + $options[$id] = check_plain($format->name); + } + + $form['quiz']['general']['default_format'] = array( + '#type' => 'select', + '#title' => t('Default input format'), + '#options' => $options, + '#default_value' => quiz_variable_get('default_format'), + '#description' => t('The default input format to use for answer and feedback fields on @quiz question forms.', array('@quiz' => QUIZ_NAME, '!format-url' => url('admin/settings/filters'))), + '#required' => TRUE + ); + } + else { + // Pass the stored filter directly. + $form['quiz']['general']['default_format'] = array( + '#type' => 'value', + '#value' => quiz_variable_get('default_format') + ); + } + + $form['quiz']['quiz_defaults'] = array( + '#type' => 'fieldset', + '#title' => t('@quiz defaults', array('@quiz' => QUIZ_NAME)), + '#collapsible' => TRUE, + '#parents' => array('quiz') + ); + + // Option to globally turn off pass / fail form elements. + $form['quiz']['quiz_defaults']['expire'] = array( + '#type' => 'textfield', + '#title' => t('Default number of days before a @quiz is closed', array('@quiz' => QUIZ_NAME)), + '#default_value' => quiz_variable_get('expire'), + '#description' => t('Supply a number of days to calculate the default close date for new quizzes.'), + '#required' => TRUE + ); + $form['quiz']['quiz_defaults']['pass_mark'] = array( + '#type' => 'select', + '#title' => t('Default percentage needed to pass a @quiz', array('@quiz' => QUIZ_NAME)), + '#options' => range(0, 100), + '#default_value' => quiz_variable_get('pass_mark'), + '#description' => t('Supply a number between 1 and 100 to set as the default percentage correct needed to pass a quiz. Set to 0 if you want to ignore pass / fail summary information by default.'), + '#required' => TRUE + ); + + return system_settings_form($form); +} + +/** + * Validation quiz configuration settings form. + */ +function quiz_admin_settings_form_validate($form_id, $form_values) { + if (!is_numeric($form_values['quiz']['expire']) || $form_values['quiz']['expire'] <= 0) { + form_set_error('quiz][expire', t('The default number of days before a quiz is closed must be a number greater than 0.')); + } +} + +/** + * Menu callback: Handle quiz taking. + * + * @todo This should probably be completely pluggable. + * @todo Avoid storing the entire question list in the session. How does one + * handle a quiz with a large or unlimited number of questions? + * + * @return + * HTML output for page. + */ +function quiz_engine() { + global $user; + + if (arg(0) == 'node' && is_numeric(arg(1)) && user_access('access quiz')) { + if ($quiz = node_load(arg(1))) { + $session_id = 'quiz_'. $quiz->nid; + + if (!isset($_SESSION[$session_id]['quiz_questions'])) { + // First time running through quiz. + if ($rid = quiz_initialise($quiz)) { + // Create question list. + $questions = quiz_build_question_list($quiz->nid); + if (count($questions) == 0) { + drupal_set_message(t('No questions have been assigned to this @quiz.', array('@quiz' => QUIZ_NAME)), 'error'); + drupal_goto('node/'. $quiz->nid); + } + else { + // Initialise session variables. + $_SESSION[$session_id]['quiz_questions'] = $questions; + $_SESSION[$session_id]['rid'] = $rid; + } + } + else { + drupal_goto('node/'. $quiz->nid); + } + } + + if (($quiz->feedback_time == FEEDBACK_AFTER_QUESTION || $quiz->feedback_time == FEEDBACK_AFTER_BOTH) && !empty($_SESSION[$session_id]['feedback'])) { + $question_node = node_load($_SESSION[$session_id]['feedback']); + unset($_SESSION[$session_id]['feedback']); + + return theme('quiz_question_feedback', $quiz, $question_node, $_SESSION[$session_id]['rid']); + } + else if (!empty($_SESSION[$session_id]['quiz_questions'])) { + // If this quiz is in progress, load the next question and return it via + // the theme. + $question_node = node_load($_SESSION[$session_id]['quiz_questions'][0]); + + return theme('quiz_take_question', $quiz, $question_node); + } + else { + // At the end of quiz. + // Get the results and summary text for this quiz. + // TODO: This can perhaps just involve a redirect to the user's results + // page for this quiz. + $questions = _quiz_get_answers($_SESSION[$session_id]['rid']); + $score = quiz_calculate_score($_SESSION[$session_id]['rid']); + $summary = _quiz_get_summary_text($quiz, $score); + + // First - update the result to show we have finished. + db_query("UPDATE {quiz_results} SET time_end = %d, score = %d WHERE rid = %d", time(), $score['percentage_score'], $_SESSION[$session_id]['rid']); + + // Get the themed summary page. + $output = theme('quiz_take_summary', $quiz, $questions, $score, $summary); + + // Remove session variables. + unset($_SESSION[$session_id]); + + return $output; + } } - elseif (($node->quiz_open < time()) && ($node->quiz_close > time())) { - $output .= t('This quiz closes %time.', array('%time' => format_date($node->quiz_close))); - } - else { - $output .= t('This quiz is no longer available.'); + } + + // The quiz does not exist or is inaccessible. + drupal_not_found(); +} + +/** + * Menu callback: Quiz admin. + */ +function quiz_admin() { + $results = _quiz_get_results(); + + return theme('quiz_admin', $results); +} + +/** + * Menu callback: Quiz results user. + */ +function quiz_user_results() { + $rid = arg(2); + + $result = db_fetch_object(db_query('SELECT q.nid + FROM {quiz} q INNER JOIN {quiz_results} qr ON (qr.quiz_nid = q.nid) + WHERE qr.rid = %d', $rid) + ); + if ($result->nid) { + $quiz = node_load($result->nid); + $questions = _quiz_get_answers($rid); + $score = quiz_calculate_score($rid); + $summary = _quiz_get_summary_text($quiz, $score); + + return theme('quiz_user_summary', $quiz, $questions, $score, $summary); + } + else { + drupal_not_found(); + } +} + +/** + * Menu callback: Quiz results admin. + */ +function quiz_admin_results() { + $rid = arg(2); + + $result = db_fetch_object(db_query('SELECT q.nid + FROM {quiz} q INNER JOIN {quiz_results} qr ON (qr.quiz_nid = q.nid) + WHERE qr.rid = %d', $rid) + ); + if ($result->nid) { + $quiz = node_load($result->nid); + $questions = _quiz_get_answers($rid); + $score = quiz_calculate_score($rid); + $summary = _quiz_get_summary_text($quiz, $score); + + return theme('quiz_admin_summary', $quiz, $questions, $score, $summary); + } + else { + drupal_not_found(); + } +} + +/** + * Menu callback: Delete result. + */ +function quiz_admin_result_delete_form() { + $form['rid'] = array('#type' => 'value', '#value' => arg(2)); + + return confirm_form( + $form, + t('Are you sure you want to delete this @quiz result?', array('@quiz' => QUIZ_NAME)), + 'admin/quiz', + t('This action cannot be undone.'), + t('Delete'), + t('Cancel') + ); +} + +/** + * Process admin_result_delete form submissions. + */ +function quiz_admin_result_delete_form_submit($form_id, $form_values) { + quiz_results_delete(NULL, $form_values['rid']); + + drupal_set_message(t('Deleted result.')); + + return 'admin/quiz'; +} + +/** + * Menu callback: Display the manage questions tab. + */ +function quiz_manage_questions() { + $status = arg(3); + $assigned = $unassigned = TRUE; + if ($status == 'assigned') { + $unassigned = FALSE; + } + else if ($status == 'unassigned') { + $assigned = FALSE; + } + + $terms = arg(4); + $terms = !empty($terms) ? explode(' ', $terms) : array(); + // This should technically be unnecessary as appropriate filtering takes place + // later as well. + $terms = array_map(create_function('$i', 'return intval($i);'), $terms); + + return drupal_get_form('quiz_manage_questions_filter_form', $assigned, $unassigned, $terms) . drupal_get_form('quiz_manage_questions_form', $assigned, $unassigned, $terms); +} + +/** + * Filter questions form for the manage questions page. + * + * @todo Streamline UI. + * + * @param $assigned + * Boolean - controls status of the 'assigned' checkbox. + * @param $unassigned + * Boolean - controls status of the 'unassigned' checkbox. + * @param $terms + * Array - Sets the default terms for the vocabulary forms. + * + * @return $form + */ +function quiz_manage_questions_filter_form($assigned, $unassigned, $terms) { + $taxonomy = _quiz_taxonomy_select($terms); + + $form = array(); + // Display filtering options. + if (!empty($taxonomy)) { + $form['taxonomy_filter'] = array( + '#type' => 'fieldset', + '#title' => t('Filter questions'), + '#description' => t('Select term(s) to filter the question list.') + ); + + $form['taxonomy_filter']['taxonomy'] = $taxonomy; + $form['taxonomy_filter']['taxonomy']['#tree'] = TRUE; + $form['taxonomy_filter']['taxonomy']['#prefix'] = '

'; + $form['taxonomy_filter']['taxonomy']['#suffix'] = '
'; + $form['taxonomy_filter']['assigned'] = array( + '#type' => 'checkbox', + '#title' => t('Display assigned questions.'), + '#default_value' => $assigned, + '#prefix' => '
' + ); + $form['taxonomy_filter']['unassigned'] = array( + '#type' => 'checkbox', + '#title' => t('Display unassigned questions.'), + '#default_value' => $unassigned, + '#suffix' => '
' + ); + $form['taxonomy_filter']['nid'] = array('#type' => 'value', '#value' => arg(1)); + $form['taxonomy_filter']['submit'] = array('#type' => 'submit', '#value' => t('Filter question list')); + } + + return $form; +} + +/** + * Process filter_form submissions and redirect appropriately. + */ +function quiz_manage_questions_filter_form_submit($form_id, $form) { + $taxonomy = isset($form['taxonomy']) ? array_filter($form['taxonomy']) : array(); + $taxonomy = implode(' ', $taxonomy); + + $status = 'assigned'; + if ($form['assigned'] && $form['unassigned']) { + $status = 'all'; + } + elseif ($form['unassigned']) { + $status = 'unassigned'; + } + + return 'node/'. $form['nid'] .'/questions/'. $status .'/'. $taxonomy; +} + +/** + * Display the questions table form. + * + * @param $assigned + * Boolean - Should questions assigned to this quiz be displayed? + * @param $unassigned + * Boolean - Should questions not assigned to this quiz be displayed? + * @param $terms + * Array - An array of terms that questions should be filtered on. + * + * @return $form + */ +function quiz_manage_questions_form($assigned, $unassigned, $terms) { + $quiz = node_load(arg(1)); + + // Set page title. + drupal_set_title(check_plain($quiz->title)); + + if ($quiz->number_of_questions) { + $description = t('The following questions are available in the question bank. This @quiz will display a maximum of %x assigned @question.', array('@quiz' => QUIZ_NAME, '%x' => check_plain($quiz->number_of_questions), '@question' => format_plural($quiz->number_of_questions, t('question'), t('questions')))); + } + else { + $description = t('The following questions are available in the question bank. This @quiz will display all questions.', array('@quiz' => QUIZ_NAME)); + } + + // Display filtered question list. + $form['filtered_question_list'] = array( + '#type' => 'fieldset', + '#title' => t('Question bank'), + '#description' => $description, + '#collapsible' => TRUE, + '#collapsed' => FALSE + ); + + $priority_descriptions = array( + QUESTION_RANDOM => t('Random'), + QUESTION_ALWAYS => t('Always'), + QUESTION_NEVER => t('Never') + ); + + // Grab current question priorities, if any. + $questions = quiz_filter_question_list($terms, $assigned, $unassigned, $quiz->nid); + $nodes = $assigned_questions = array(); + foreach ($questions as $question) { + $nodes[$question->nid] = ''; + if ($question->question_priority != QUESTION_NEVER) { + $assigned_questions[$question->nid] = TRUE; } + $form['filtered_question_list']['question'][$question->nid] = array('#value' => l($question->title, 'node/'. $question->nid)); + $form['filtered_question_list']['type'][$question->nid] = array('#value' => $question->type); + $form['filtered_question_list']['question_priority'][$question->nid] = array('#value' => $priority_descriptions[$question->question_priority]); } - $output .= '

'."\n"; - return $output; + $form['filtered_question_list']['nodes'] = array('#type' => 'checkboxes', '#options' => $nodes); + + // Store assigned questions to avoid additional DB calls during form_submit. + $form['assigned_questions'] = array('#type' => 'value', '#value' => $assigned_questions); + + $form['controls'] = array('#type' => 'fieldset'); + $form['controls']['operation'] = array( + '#type' => 'select', + '#title' => t('With selected'), + '#options' => array( + 'assign-'. QUESTION_RANDOM => t('Assign as random questions'), + 'assign-'. QUESTION_ALWAYS => t('Assign as always-asked questions'), + 'assign-'. QUESTION_NEVER => t('Unassign') + ), + '#prefix' => '
' + ); + + $form['controls']['submit'] = array( + '#type' => 'submit', + '#value' => t('Update'), + '#suffix' => '
' + ); + + return $form; +} + +/** + * Validate the manage questions form. + */ +function quiz_manage_questions_form_validate($form_id, $form_values) { + $questions = array_filter($form_values['nodes']); + if (empty($questions)) { + form_set_error('', t('No questions were selected. Please select a question and try again.')); + } +} + +/** + * Process manage questions form submission. + * @todo Replace 'always' and 'random' with priority levels. + */ +function quiz_manage_questions_form_submit($form_id, $form_values) { + $questions = array_filter($form_values['nodes']); + $operation = explode('-', $form_values['operation']); + + // Load the node. + $quiz = node_load(arg(1)); + + // Update quiz with selected question options. + quiz_update_questions($quiz, $questions, $operation[1], $form_values['assigned_questions']); + drupal_set_message(t('Questions updated successfully.')); + + $min_questions = quiz_validate_max_questions($quiz); + if ($min_questions !== TRUE) { + db_query("UPDATE {quiz} SET number_of_questions = %d WHERE nid = %d", $min_questions, $quiz->nid); + drupal_set_message(t('The number of questions for this @quiz has been increased to %min_questions to match the number of questions set to be asked always.', array('@quiz' => QUIZ_NAME, '%min_questions' => $min_questions))); + } +} + +/** + * Called by question modules. Store question result in the database and move + * to the next question. + * + * @param $quiz_nid + * The node ID of the quiz. + * @param $question + * The question node. + * @param $input + * An array of user-selected answers. + */ +function quiz_update_result($quiz_nid, $question, $input) { + $session_id = 'quiz_'. $quiz_nid; + // TODO: When backwards navigation is introduced, an equivalent to REPLACE + // will need to be used. + db_query("INSERT INTO {quiz_question_results} (rid, question_nid, question_vid, tries) VALUES(%d, %d, %d, '%s')", $_SESSION[$session_id]['rid'], $question->nid, $question->vid, serialize($input)); + + // Set feedback trigger and move to the next question. + // FIXME: This is very dodgy. + $_SESSION[$session_id]['feedback'] = array_shift($_SESSION[$session_id]['quiz_questions']); +} + +/** + * Return a quiz module variable. + * + * @param $name + * The name of the variable to retrieve. + * @return + * The value of the variable requested. + */ +function quiz_variable_get($name) { + static $variables = array(); + + if (empty($variables)) { + $defaults = array( + 'name' => 'Quiz', + 'expire' => 30, + 'pass_mark' => 80, + 'pager' => 5, + 'default_format' => variable_get('filter_default_format', 1) + ); + $variables = variable_get('quiz', array()); + $variables = array_merge($defaults, $variables); + } + + return $variables[$name]; } /** - * Theme the node view for quizzes + * Since Drupal core does not cache node_loads which include a revision + * argument, we use a home-grown version. + * + * @param $nid + * The node ID of the node to retrieve. + * @param $vid + * The revision ID of the node to retrieve. + * + * @return + * The node object. */ -function theme_quiz_view($node, $teaser = FALSE, $page = FALSE) { - $output = ''; - // Ouput quiz options - $output .= '

'. t('@quiz Options', array('@quiz' => QUIZ_NAME)) .'

'; - $header = array( - t('# of Questions'), - t('Shuffle?'), - t('Number of takes') - ); - $shuffle = $node->shuffle == 1 ? t('Yes') : t('No'); - $takes = $node->takes == 0 ? t('Unlimited') : check_plain($node->takes); - $rows = array(); - $rows[] = array( - check_plain($node->number_of_questions), - $shuffle, - $takes - ); - $output .= theme('table', $header, $rows); - // Format Quiz Dates - $output .= '

'. t('@quiz start/end', array('@quiz' => QUIZ_NAME)) .'

'; - if (!$node->quiz_always) { - // if we are previewing, make sure the dates are timestamps and not form arrays - if (is_array($node->quiz_open)) { - quiz_translate_form_date($node, 'quiz_open'); - } - if (is_array($node->quiz_close)) { - quiz_translate_form_date($node, 'quiz_close'); - } +function quiz_load_revision($nid, $vid) { + static $nodes = array(); - // format the availability info - $output .= '

'. format_date($node->quiz_open) .' — '. format_date($node->quiz_close) .'

'; - $output .= '

' . t('Days @quiz live for: ', array('@quiz' => QUIZ_NAME)) . ' ' . floor(($node->quiz_close - $node->quiz_open) / 60 / 60 / 24) . '

'; - $remaining = floor(($node->quiz_close - time()) / 60 / 60 / 24); - $remaining = ($remaining < 0)?'Expired':$remaining; - $output .= '

Days remaining: '. $remaining .'

'; - $elapsed = floor((time() - $node->quiz_open) / 60 / 60 / 24); - $elapsed = ($elapsed < 0)?(-$elapsed) .' days to go':$elapsed; - $output .= '

Days since start: '. $elapsed .'

'; - } - else { - $output .= '

'. t('This Quiz is always available.') .'

'."\n"; + if (!isset($nodes[$nid]) || !isset($nodes[$nid][$vid])) { + $nodes[$nid][$vid] = node_load($nid, $vid); } - // Format taxonomy selection (if applicable) - if (function_exists(taxonomy_node_get_terms)) { - $output .= '

'. t('Taxonomy selection') .'

'; - $terms = array(); - foreach (taxonomy_node_get_terms($node->nid) as $term) { - $terms[] = check_plain($term->name); - } - if (!empty($terms)) { - $terms = implode(', ', $terms); - $output .= "

$terms

"; - } - else { - $output .= '

'. t('No selected terms found') .'

'; - } - } + return $nodes[$nid][$vid]; +} - // Format pass / fail and summary options - if ($node->pass_rate || $node->summary_default || $node->summary_pass) { - if ($node->pass_rate) { - $output .= '

'. t('Pass / fail and summary options') .'

'."\n"; - $output .= '

'. t('Percentage needed to pass:') .' '. check_plain($node->pass_rate) .'

'."\n"; - $output .= '
'. t('Summary text if the user passed:') .' '; - $output .= ($node->summary_pass) ? check_markup($node->summary_pass) : t('No text defined.'); - $output .= '
'."\n"; +/** + * Delete results from the results tables. + * + * @param $quiz_nid + * Node ID of the quiz whose results are to be deleted (Optional). + * @param $rid + * Result ID of the result which is to be deleted (Optional). + */ +function quiz_results_delete($quiz_nid = NULL, $rid = NULL) { + $rids = array(); + if ($quiz_nid) { + $result = db_query("SELECT rid FROM {quiz_results} WHERE quiz_nid = %d", $quiz_nid); + while ($row = db_fetch_array($result)) { + $rids[] = $row['rid']; } - $output .= '
'. t('Default summary text:') .' '; - $output .= ($node->summary_default) ? check_markup($node->summary_default) : t('No text defined.'); - $output .= '
'."\n"; + db_query("DELETE FROM {quiz_results} WHERE quiz_nid = %d", $quiz_nid); } - - // Format quiz questions - if (is_numeric(arg(1))) { - $output .= '

'. t('@quiz Questions', array('@quiz' => QUIZ_NAME)) .'

'; - $questions = _quiz_get_questions(); - $output .= theme('quiz_question_table', $questions); + else if ($rid) { + db_query("DELETE FROM {quiz_results} WHERE rid = %d", $rid); + $rids[] = $rid; + } + foreach ($rids as $rid) { + db_query("DELETE FROM {quiz_question_results} WHERE rid = %d", $rid); } - return $output; } - /** - * Displays all the quizs the user has taken part in + * Displays all the quizzes the user has taken part in. * * @return - * HTML output for page - */ + * HTML output for page. + */ function quiz_get_user_results() { global $user; + $results = array(); - $dbresult = db_query("SELECT - n.nid nid, - n.title title, - u.name name, - qr.rid rid, - qr.time_start, - qr.time_end - FROM {node} n, {quiz} q, {quiz_result} qr, {users} u - WHERE - n.type = 'quiz' - AND - n.nid = q.nid - AND - qr.quiz_nid = q.nid - AND - u.uid = qr.uid - AND - u.uid = ". $user->uid ." - ORDER BY qr.rid ASC"); - //Create results array - while ($line = db_fetch_array($dbresult)) { - $results[$line['rid']] = $line; + $sql = "SELECT n.nid, n.title, qr.rid, qr.score, qr.time_start, qr.time_end + FROM {quiz_results} qr INNER JOIN {node} n ON (qr.quiz_nid = n.nid) INNER JOIN {quiz} q ON (n.vid = q.vid) + WHERE qr.uid = %d + ORDER BY qr.rid DESC + "; + + $result = pager_query($sql, quiz_variable_get('pager'), 0, NULL, $user->uid); + + // Create results array. + while ($line = db_fetch_object($result)) { + $results[$line->rid] = $line; } + return theme('quiz_get_user_results', $results); - } /** - * Handles quiz taking + * Get the summary message for a completed quiz. + * + * Summary is determined by whether we are using the pass / fail options, how + * the user did, and whether this is being called from admin/quiz/[quizid]/view. + * + * @todo Need better feedback for when a user is viewing their quiz results from + * the results list (and possibly when revisiting a quiz they can't take + * again). * - * @return - * HTML output for page - */ -function quiz_take_quiz() { - global $user; - if (arg(0) == 'node' && is_numeric(arg(1)) && user_access('access quiz')) { - if ($quiz = node_load(arg(1))) { - if (!isset($_SESSION['quiz_'. $quiz->nid]['quiz_questions'])) { - - // First time running through quiz - if ($rid = quiz_start_actions($user->uid, $quiz->nid)) { - // Create question list - $questions = quiz_build_question_list($quiz->nid); - if (count($questions) == 0) { - drupal_set_message(t('No questions were found. Please assign questions before trying to take this @quiz.', array('@quiz' => QUIZ_NAME)), 'error'); - return ''; - } - // Initialize session variables - $_SESSION['quiz_'. $quiz->nid]['quiz_questions'] = $questions; - $_SESSION['quiz_'. $quiz->nid]['rid'] = $rid; - $_SESSION['quiz_'. $quiz->nid]['question_number'] = 0; - - } - else { - return ''; - } - } - - // Check for answer submission - if ($_POST['op'] == t('Submit')) { - if (!isset($_POST['tries'])) { - drupal_set_message('You must select an answer before you can progress to the next question!', 'error'); - } - else { - $former_question = node_load(array('nid' => array_shift($_SESSION['quiz_'. $quiz->nid]['quiz_questions']))); - $result = module_invoke($former_question->type, 'evaluate_question', $former_question->nid); - db_query("REPLACE {quiz_question_results} VALUES(%d, %d, '%s')", $_SESSION['quiz_'. $quiz->nid]['rid'], $former_question->nid, serialize($result)); - } - } - - // If this quiz is in progress, load the next questions - // and return it via the theme - if (!empty($_SESSION['quiz_'. $quiz->nid]['quiz_questions'])) { - - $question_node = node_load(array('nid' => $_SESSION['quiz_'. $quiz->nid]['quiz_questions'][0])); - return theme('quiz_take_question', $quiz, $question_node); - } - - // At the end of quiz - else { - //First - update the result to show we have finished. - $now = time(); - db_query("UPDATE {quiz_result} SET time_end = %d WHERE rid = %d", $now, $_SESSION['quiz_'. $quiz->nid]['rid']); - - //Get the results and summary text for this quiz - $questions = _quiz_get_answers($_SESSION['quiz_'. $quiz->nid]['rid']); - $score = quiz_calculate_score($_SESSION['quiz_'. $quiz->nid]['rid']); - $summary = _quiz_get_summary_text($quiz, $score); - - // get the themed summary page - $output = theme('quiz_take_summary', $quiz, $questions, $score, $summary); - - //remove session variables - unset($_SESSION['quiz_'. $quiz->nid]); - - // return - return $output; - - } - } - } - // If we got down here then the quiz does not exist. - drupal_not_found(); -} - - -/*** - * Get the summary message for a completed quiz - * - * Summary is determined by whether we are using the - * pass / fail options, how the user did, and - * whether this is being called from admin/quiz/[quizid]/view. - * - * TODO: Need better feedback for when a user is viewing - * their quiz results from the results list (and possibily - * when revisiting a quiz they can't take again) - * * @param $quiz - * The quiz node object + * The quiz node object. * @param $score - * The score information as returned by quiz_calculate_score() + * The score information as returned by quiz_calculate_score(). * @return - * Filtered summary text or null if we are not displaying any summary + * Filtered summary text or null if we are not displaying any summary. */ function _quiz_get_summary_text($quiz, $score) { - $summary = ''; - // if we are using pass / fail and they passed - if (trim($quiz->summary_pass) != '' && $quiz->pass_rate > 0 && $score['percentage_score'] >= $quiz->pass_rate) { - // If we are coming from the admin view page + // If the user has passed; we are using pass / fail; the summary message is + // not empty. + if ($score['percentage_score'] >= $quiz->pass_rate && $quiz->summary_pass && $quiz->pass_rate > 0) { + // If we are coming from the admin view page. if (arg(3) == 'view') { - $summary = t('The user passed this quiz.'); + $summary = t('The user passed this quiz.'); } else { - $summary = check_markup($quiz->summary_pass, $quiz->format); + $summary = check_markup($quiz->summary_pass, $quiz->format, FALSE); } - } - // If the user did not pass or we are not using pass / fail - else { - // If we are coming from the admin view page - // only show a summary if we are using pass / fail. + } + // If the user did not pass or we are not using pass / fail and the default + // message is not empty. + else if ($quiz->summary_default) { + // If we are coming from the admin view page, only show a summary if we are + // using pass / fail. if (arg(3) == 'view') { if ($node->pass_rate > 0) { $summary = t('The user failed this quiz.'); } } else { - $summary = check_markup($quiz->summary_default, $quiz->format); + $summary = check_markup($quiz->summary_default, $quiz->format, FALSE); } } + return $summary; } /** - * Actions to take place at the start of a quiz + * Actions to take place at the start of a quiz. * - * @param $uid - * User ID - * @param $nid - * Quiz node ID + * @param $quiz + * Quiz node. * @return integer * Returns quiz_result rid, or false if there is an error. */ -function quiz_start_actions($uid, $nid) { - // get the quiz node - $quiz = node_load($nid); +function quiz_initialise($quiz) { + global $user; - // make sure this is available - if (!$quiz->quiz_always == 1) { - // compare current gm time to open and close dates (which should still be in gm time) - if (gmmktime() >= $quiz->quiz_close || gmmktime() < $quiz->quiz_open) { - drupal_set_message(t('This @quiz is not currently available.', array('@quiz' => QUIZ_NAME)), 'status'); - if (!user_access('create quiz')) { - return FALSE; - } - } - } + $quiz_available = quiz_validate_availability($quiz); + if ($quiz_available !== TRUE) { + drupal_set_message($quiz_available); - // get the results - global $user; - $results = _quiz_get_results($quiz->nid, $user->uid); - // Check to see if the user alredy passed this quiz - // but only perform this check if it is a registered user - if ($user->uid) { - $passed = FALSE; - while ($next = next($results)) { - $score = quiz_calculate_score($next['rid']); - if ($score['percentage_score'] >= $quiz->pass_rate) { - $passed = TRUE; - break; - } - } - if ($passed == TRUE) { - drupal_set_message(t('You have already passed this @quiz.', array('@quiz' => QUIZ_NAME)), 'status'); - // Allow quiz creators to test their quizzes - if (!user_access('create quiz')) { - return FALSE; - } - } + return FALSE; } - // Validate number of takes - if ($quiz->takes != 0) { - //$result = db_result(db_query('SELECT COUNT(rid) AS count FROM {quiz_result} WHERE uid = %d AND quiz_nid = %d', $uid, $nid)); - $times = count($results); - if ($times >= $quiz->takes) { - drupal_set_message(t('You have already taken this @quiz %d times.', array('@quiz' => QUIZ_NAME, '%d' => $times)), 'status'); - // Allow quiz creators to test their quizzes - if (!user_access('create quiz')) { - return FALSE; - } - } - } + $quiz_takes_available = quiz_validate_takes($quiz, $user->uid); + if ($quiz_takes_available !== TRUE) { + drupal_set_message($quiz_takes_available); - // Insert quiz_results record - $rid = db_next_id('{quiz_results}_rid'); - $now = time(); - $result = db_query("INSERT INTO {quiz_result} (rid, quiz_nid, uid, time_start) VALUES (%d, %d, %d, %d)", $rid, $nid, $uid, $now); - if ($result) { - return $rid; - } - else { - drupal_set_message(t('There was a problem starting the @quiz. Please try again later.', array('@quiz' => QUIZ_NAME), 'error')); return FALSE; } + + // Initialise quiz_results record. + $rid = db_next_id('{quiz_results}_rid'); + $result = db_query("INSERT INTO {quiz_results} (rid, quiz_nid, uid, time_start) VALUES (%d, %d, %d, %d)", $rid, $quiz->nid, $user->uid, time()); + + return $rid; } /** - * Calculates the score user received on quiz + * Calculates the score user received on quiz. * * @param $rid - * Quiz result ID + * Quiz result ID. * @return array - * Contains three elements: question_count, num_correct and percentage_score + * Contains three elements: question_count, num_correct and percentage_score. */ function quiz_calculate_score($rid) { - // initialize our variables - $question_count = 0; - $num_correct = 0; - $percentage_score = 0; - - // Get the all answers from the database - $result = db_query("SELECT - qqr.answer answer, - qqr.question_nid qnid, - n.type type - FROM {quiz_question_results} qqr, {node} n - WHERE qqr.result_rid = %d AND n.nid = qqr.question_nid", $rid); + // Initialise our variables. + $question_count = $num_correct = $percentage_score = 0; + + // Get the answers from the database. + $result = db_query("SELECT qqr.tries, qqr.question_nid, qqr.question_vid + FROM {quiz_question_results} qqr + WHERE qqr.rid = %d", $rid + ); + while ($r = db_fetch_array($result)) { - $question_count++; - $r['answer'] = unserialize($r['answer']); - $s = module_invoke($r['type'], 'calculate_result', $r['answer']['answers'], $r['answer']['tried']); - $num_correct += $s; - $r['score'] = $s; // I think this is legacy + if ($question = quiz_load_revision($r['question_nid'], $r['question_vid'])) { + $question_count++; + $outcome = module_invoke($question->type, 'calculate_result', $question->answers, unserialize($r['tries'])); + if ($outcome) { + $num_correct++; + } + } } - - // calculate the percentage score + + // Calculate the percentage score. if ($question_count > 0) { $percentage_score = round(($num_correct * 100) / $question_count); } - // build the score array + // Build the score array. $score = array( 'question_count' => $question_count, - 'num_correct' => $num_correct, - 'percentage_score' => $percentage_score, + 'num_correct' => $num_correct, + 'percentage_score' => $percentage_score ); - // return the array return $score; } /** - * Retrieves a question list for a given quiz + * Retrieves a question list for a given quiz. * * @param $nid - * Quiz node ID + * Quiz node ID. * @return - * Array of question node IDs + * Array of question node IDs. */ function quiz_build_question_list($nid) { $questions = array(); $quiz = node_load($nid); - // Get required questions first - $result = db_query("SELECT question_nid FROM {quiz_questions} WHERE quiz_nid = %d AND question_status = %d", $nid, QUESTION_ALWAYS); + // Get required questions first. + $result = db_query("SELECT question_nid FROM {quiz_questions} WHERE quiz_nid = %d AND question_priority = %d", $nid, QUESTION_ALWAYS); while ($question_node = db_fetch_object($result)) { $questions[] = $question_node->question_nid; } - // Get random questions for the remainder - $quiz->number_of_questions -= count($questions); - $result = db_query_range("SELECT question_nid FROM {quiz_questions} WHERE quiz_nid = %d AND question_status = %d ORDER BY RAND()", $nid, QUESTION_RANDOM, 0, $quiz->number_of_questions); + // Get random questions for the remainder. + $quiz->max_questions -= count($questions); + $result = db_query_range("SELECT question_nid FROM {quiz_questions} WHERE quiz_nid = %d AND question_priority = %d ORDER BY RAND()", $nid, QUESTION_RANDOM, 0, $quiz->max_questions); while ($question_node = db_fetch_object($result)) { $questions[] = $question_node->question_nid; } - // Shuffle questions if required + // Shuffle questions if required. if ($quiz->shuffle == 1) { shuffle($questions); } @@ -877,120 +1269,22 @@ } /** - * Implementation of hook_help(). - */ -function quiz_help($section) { - switch ($section) { - case 'admin/help#quiz': - return t(' -

Description

-

The quiz module allows users to administer a quiz, as a sequence of questions, and track the answers given. It allows for the creation of questions (and their answers), and organizes these questions into a quiz. Finally, it provides a mechanism for ensuring question quality through a combination of community revision and moderation. Its target audience includes educational institutions, online training programs, employers, and people who just want to add a fun activity for their visitors to their Drupal site.

-

Creating Your First Quiz

-

Creating an initial quiz requires three steps:

-
    -
  1. Create at least one taxonomy vocabulary and assign it to the quiz and question type modules
  2. -
  3. Create a series of questions
  4. -
  5. Create a quiz based on the series of questions
  6. -
-

Also note that for anyone but the site administrator, creating quizzes requires the create quiz privilege, and creating questions requires the administer question type privilege. These settings can be configured in Administer >> User management >> Access control.

-

Setting up a vocabulary

-
    -
  1. If not already enabled, go to the Administer >> Site building >> Modules section of the control panel and check the enable checkbox to enable the taxonomy module.
  2. -
  3. If you do not already have a taxonomy vocabulary suitable for quizzes, go to Administer >> Content management >> Categories and create a vocabulary for quizzes (for example, Quiz Topics). Ensure that under Types, both quiz and all question types (for example, multichoice) are selected. Depending on your needs, you may wish to create a hierarchical vocabulary, so that topics can be sub-divided into smaller areas, and/or enable multiple select to associate quizzes and questions with more than one category.
  4. -
  5. Add a series of terms to the vocabulary to which questions and quizzes can be assigned. For example: -
-

Creating quiz questions

-
    -
  1. Begin by clicking Create content, and then select a question type node (for example, multichoice)
  2. -
  3. Fill out the question form. The presented interface will vary depending on the question type, but for multiple choice questions: -
    -
    Title
    -
    Question title. This will be displayed as the heading of the question.
    -
    Taxonomy selection
    -
    Any taxonomy vocabularies that are assigned to the question type will be displayed.
    -
    Question
    -
    The actual question text (for example, What is 2+2?).
    -
    Multiple Answers
    -
    Whether or not the question has multiple correct answers, such as a "Select all that apply" question.
    -
    Correct
    -
    Indicates that given answer is a correct answer.
    -
    Answer
    -
    An answer choice (for example, 4). If more answers are required, check I need more answers and click the Preview button.
    -
    Feedback
    -
    Feedback, if supplied, will be provided to the user at the end of the quiz.
    -
    -
  4. -
  5. Repeat for each question you would like included on the quiz.
  6. -
-

Creating the quiz

-
    -
  1. Go to Create content >> Quiz to access the quiz creation form.
  2. -
  3. Fill out the form to set the @quiz options: -
    -
    Title
    -
    Quiz title. This will be displayed as the heading of the quiz.
    -
    Taxonomy selection
    -
    Any taxonomy vocabularies that are assigned to the quiz type will be displayed. Select from the terms displayed in order to assign the quiz to vocabulary terms.
    -
    Number of questions
    -
    Total number of questions on quiz.
    -
    Shuffle questions
    -
    Whether or not to shuffle (randomize) the questions.
    -
    Number of takes
    -
    Number of takes to allow user. Varies from 1-9 or Unlimited times.
    -
    -
  4. -
  5. Once the quiz has been created, click the add questions tab to assign questions to the quiz.
  6. -
  7. Select a radio button next to each question indicating if the question should appear (Randomly, Always, or Never), and click Submit questions.
  8. -
  9. Repeat process until satisfied with question selection.
  10. -
- ', array('@quiz' => QUIZ_NAME, '@admin-access' => url('admin/user/access'), '@admin-modules' => url('admin/build/modules'), '@admin-taxonomy' => url('admin/content/taxonomy'), '@create-content' => url('node/add'), '@multichoice' => url('node/add/multichoice'), '@create-quiz' => url('node/add/quiz'))); - case 'node/add#quiz': - return t('A collection of questions designed to create interactive tests'); - default: - break; - } -} - -/** - * Retrieve list of question types + * Retrieve list of question types. * - * Determined by which modules implement the list_questions() hook + * Determined by which modules implement the render_question() hook. * * @return - * Array of question types + * Array of question modules. */ function _quiz_get_question_types() { - return module_implements('list_questions'); + return module_implements('render_question'); } /** - * Retrieve list of vocabularies for all quiz question types + * Retrieve list of vocabularies for all quiz question types. * * @return - * An array containing vocabulary list + * An array containing vocabulary list. */ function _quiz_get_vocabularies() { $vocabularies = array(); @@ -999,372 +1293,174 @@ $vocabularies[$vid] = $vocabulary; } } + return $vocabularies; } /** - * Prints a taxonomy selection form for each vocabulary + * Prints a taxonomy selection form for each vocabulary. * - * @param $value - * Default selected value(s) + * @param $terms + * Taxonomy terms in a space separated string. * @return - * HTML output to print to screen + * Form array. */ -function _quiz_taxonomy_select($value = 0) { +function _quiz_taxonomy_select($terms = array()) { $form = array(); + foreach (_quiz_get_vocabularies() as $vid => $vocabulary) { - $form['taxonomy'][$vid] = taxonomy_form($vid, $value); + // Render vocabulary form. Unlike taxonomy_form_all, free tag vocabularies + // are rendered incorrectly, which might be intentional. + // The third parameter (' ') is to ensure that the vocabulary help text + // is not to be displayed. It requires a string with a single space - a + // taxonomy module bug. + $form[$vid] = taxonomy_form($vid, $terms, ' '); } + return $form; } /** - * Retrieve list of questions assigned to quiz + * Gets the number questions of a given type for a quiz. * + * @param $nid + * Node ID of the quiz. + * @param $priority + * Priority constant. * @return - * Array of questions + * Number of questions that meet the criteria. */ -function _quiz_get_questions() { - $quiz = node_load(arg(1)); - $questions = array(); - if (!empty($quiz->nid)) { - // Retrieve list of questions - $result = db_query(" - SELECT n.nid, n.type, nr.body, nr.format, q.question_status - FROM {node} n, {node_revisions} nr, {quiz_questions} q - WHERE n.nid = q.question_nid - AND n.nid = nr.nid - AND q.quiz_nid = %d", $quiz->nid - ); - - // Create questions array - while ($node = db_fetch_object($result)) { - $questions[] = quiz_node_map($node); - } - } - - return $questions; +function quiz_get_num_questions($nid, $priority) { + return db_result(db_query("SELECT COUNT(qq.question_nid) as count FROM {quiz_questions} qq INNER JOIN {node} n ON (n.nid = qq.question_nid) WHERE qq.quiz_nid = %d AND qq.question_priority = %d AND n.status = 1", $nid, $priority)); } - /** - * Handles "Manage questions" tab + * Filters question list by given terms. * - * Displays form which allows questions to be assigned to the given quiz + * @param $terms + * An array of vocabulary term IDs. + * @param $assigned + * Should assigned questions be retrieved? + * @param $unassigned + * Should unassigned questions be retrieved? + * @param $quiz_nid + * The node ID of the quiz node being managed. * * @return - * HTML output to create page + * Array of questions which match terms. */ -function quiz_questions_form() { - $quiz = node_load(arg(1)); - - // Set page title - drupal_set_title(check_plain($quiz->title)); - - // show the number of questions that this quiz currently has - $form['numberofquestions'] = array( - '#prefix' => '
', - '#value' => t('This @quiz consists of %x @question.', array('@quiz' => QUIZ_NAME, '%x' => check_plain($quiz->number_of_questions), '@question' => format_plural($quiz->number_of_questions, t('question'), t('questions')))), - '#suffix' => '

'."\n" - ); - - // Retrieve question list from database - $sql = "SELECT DISTINCT n.nid, n.type, r.body, r.format FROM {node} n, {node_revisions} r WHERE n.nid = r.nid AND n.type IN ('". implode("','", _quiz_get_question_types()) ."') "; - $result = db_query($sql); - - // Create questions array - $questions = array(); - while ($node = db_fetch_object($result)) { - $questions[$node->nid] = quiz_node_map($node); - } - - $result = db_query('SELECT question_nid, question_status FROM {quiz_questions} WHERE quiz_nid = %d', $quiz->nid); - while ($assigned_question = db_fetch_object($result)) { - if (array_key_exists($assigned_question->question_nid, $questions)) { - $the_question =& $questions[$assigned_question->question_nid]; - $the_question->status = $assigned_question->question_status; - } - } - - - // Display filtered question list - $form['filtered_question_list'] = array( - '#type' => 'fieldset', - '#title' => t('The following questions were found'), - '#theme' => 'quiz_filtered_questions', - ); - - $form['filtered_question_list']['question_status']['#tree'] = TRUE; - - while (list($key, $question) = each($questions)) { - $form['filtered_question_list']['question_status'][$question->nid] = array( - '#type' => 'radios', - '#options' => array(QUESTION_RANDOM => '', QUESTION_ALWAYS => '', QUESTION_NEVER => ''), - '#default_value' => $question->status, - ); - $form['filtered_question_list']['question'][$question->nid] = array( - '#type' => 'markup', - '#value' => $question->question, - ); - $form['filtered_question_list']['type'][$question->nid] = array( - '#type' => 'markup', - '#value' => $question->type, - ); - } - - // Get questions assigned to this quiz - $questions = _quiz_get_questions(); - - // Display questions - $form['assigned_questions'] = array( - '#type' => 'fieldset', - '#title' => t('Questions assigned to this @quiz', array('@quiz' => QUIZ_NAME)), - ); - $form['assigned_questions']['questions'] = array( - '#type' => 'markup', - '#value' => theme('quiz_question_table', $questions), - ); - - // Display links to create other questions - $form['additional_questions'] = array( - '#type' => 'fieldset', - '#title' => t('Create additional questions'), - '#theme' => 'additional_questions', - ); - foreach (_quiz_get_question_types() as $type) { - $form['additional_questions'][$type] = array( - '#type' => 'markup', - '#value' => l($type, 'node/add/' . $type', array('title' => t("Go to " . $type . " administration"))) .' ', - ); - } - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Submit questions'), - ); - return $form; -} - -function quiz_questions() { - return drupal_get_form('quiz_questions_form'); -} +function quiz_filter_question_list($terms, $assigned, $unassigned, $quiz_nid) { + // Retrieve question list from database, filtered on vocabulary terms. + $sql = "SELECT DISTINCT n.nid, n.title, n.type, nr.body, nr.format, qq.question_priority FROM {node} n"; + $joins[] = "INNER JOIN {node_revisions} nr ON (n.nid = nr.nid)"; + // Select published question nodes only. + $wheres[] = "n.status = 1"; + $wheres[] = "n.type IN ('". implode("','", _quiz_get_question_types()) ."')"; + $args = array(); -/** - * Submit function for quiz_questions - * - * Updates from the "add questions" tab - * - * @param $form_id - * A string containing the form id - * @param $values - * Array containing the form values - */ -function quiz_questions_form_submit($form_id, $values) { - // load the node - $quiz = node_load(arg(1)); - // Update quiz with selected question options - if (!quiz_update_questions($values['question_status'])) { - form_set_error('', t('Either no questions were selected, or there was a problem updating your @quiz. Please try again.', array('@quiz' => QUIZ_NAME))); - return; + if (count($terms) > 0) { + $joins[] = "INNER JOIN {term_node} t ON (t.nid = nr.nid)"; + $wheres[] .= "t.tid IN (". implode(', ', $terms) .")"; + } + if ($assigned && !$unassigned) { + $joins[] = "INNER JOIN {quiz_questions} qq ON (n.nid = qq.question_nid)"; + $wheres[] = "qq.quiz_nid = %d"; + $args[] = $quiz_nid; + } + else if ($unassigned && !$assigned) { + // Unassigned questions will not necessarily be present in the + // quiz_questions table. Therefore, use a LEFT JOIN. + $joins[] = "LEFT JOIN {quiz_questions} qq ON (n.nid = qq.question_nid)"; + $wheres[] = "qq.question_nid IS NULL"; } - - // Determine how many more questions are required - $qnum = $quiz->number_of_questions; - $anum_random = quiz_get_num_questions($quiz->nid, QUESTION_RANDOM); - $anum_always = quiz_get_num_questions($quiz->nid, QUESTION_ALWAYS); - $anum_total = $anum_always + $anum_random; - - // If we have some random questions, increase this number by one. - if ($anum_random > 0) { - $anum_always++; + else { + $joins[] = "LEFT JOIN {quiz_questions} qq ON (n.nid = qq.question_nid)"; + $wheres[] = "(qq.quiz_nid = %d OR qq.question_nid IS NULL)"; + $args[] = $quiz_nid; } - // If there are not enough questions, lower the number of questions and let the user know. - if ($anum_total < $qnum) { - drupal_set_message(t('The number of questions for this @quiz have been lowered to %anum to match the number of questions you assigned.', array('@quiz' => QUIZ_NAME, '%anum' => $anum_total)), 'status'); - db_query("UPDATE {quiz} SET number_of_questions = %d WHERE nid = %d", $anum_total, $quiz->nid); - } + $joins = implode(' ', $joins); + $wheres = implode(' AND ', $wheres); - // If there are too many questions set to "always", increase the number of questions and let the user know. - else if ($anum_always > $qnum) { - drupal_set_message(t('The number of questions for this @quiz have been increased to %anum to match the number of questions you assigned.', array('@quiz' => QUIZ_NAME, '%anum' => $anum_always)), 'status'); - db_query("UPDATE {quiz} SET number_of_questions = %d WHERE nid = %d", $anum_always, $quiz->nid); - } + $result = pager_query("$sql $joins WHERE $wheres ORDER BY n.title ASC", quiz_variable_get('pager'), 0, NULL, $args); - // Otherwise just give general feedback. - else { - drupal_set_message(t('Questions updated successfully.')); + // Create questions array. + $questions = array(); + while ($node = db_fetch_object($result)) { + $questions[$node->nid] = quiz_node_map($node); } -} -/** - * Gets the number questions of a given type for a quiz - * - * @param $nid - * node id of the quiz - * @param $type - * status constant - * @return - * Number of questions that meet the criteria - */ -function quiz_get_num_questions($nid, $type) { - return db_num_rows(db_query("SELECT question_nid FROM {quiz_questions} WHERE quiz_nid = %d AND question_status = %d", $nid, $type)); + return $questions; } /** - * Map node properties to a question object + * Map node properties to a question object. * * @param $node - * Node + * Node. * @return - * Question object + * Question object. */ function quiz_node_map($node) { $new_question = new stdClass(); - $new_question->question = check_markup($node->body, $node->format); + $new_question->title = $node->title; + $new_question->question = check_markup($node->body, $node->format, FALSE); $new_question->nid = $node->nid; $new_question->type = $node->type; - $new_question->status = isset($node->question_status) ? $node->question_status : 2; + $new_question->question_priority = isset($node->question_priority) ? $node->question_priority : QUESTION_NEVER; + return $new_question; } /** - * Updates questions assigned to the quiz + * Updates questions assigned to the quiz. * + * @param $quiz + * Quiz node. * @param $questions - * Array of questions and their status - * @return - * True if update was a success, false if three was a problem - */ -function quiz_update_questions($questions) { - $quiz = node_load(arg(1)); - $return = true; - - if (!empty($questions)) { - // Get currently assigned questions - $result = db_query("SELECT quiz_nid, question_nid, question_status FROM {quiz_questions} WHERE quiz_nid = %d", $quiz->nid); - $assigned_questions = array(); - while ($assigned_question = db_fetch_object($result)) { - $assigned_questions[$assigned_question->question_nid] = $assigned_question->question_status; - } - - // Perform update if question is already assigned, or insert if it's a new question - while (list($key, $value) = each($questions)) { - if ($value == QUESTION_NEVER) { - $result = db_query("DELETE FROM {quiz_questions} WHERE quiz_nid = %d AND question_nid = %d", $quiz->nid, $key); - if (!$result) { - $return = false; - } + * Array of questions to update. + * @param $priority + * The priority level to assign to these questions. + * @param $assigned_questions + * An array of currently assigned questions. + */ +function quiz_update_questions($quiz, $questions, $priority, $assigned_questions) { + foreach ($questions as $question_nid) { + if ($priority == QUESTION_NEVER) { + db_query("DELETE FROM {quiz_questions} WHERE quiz_nid = %d AND question_nid = %d", $quiz->nid, $question_nid); + } + else { + if (isset($assigned_questions[$question_nid])) { + db_query("UPDATE {quiz_questions} SET question_priority = %d WHERE quiz_nid = %d AND question_nid = %d", $priority, $quiz->nid, $question_nid); } else { - if (array_key_exists($key, $assigned_questions)) { - $result = db_query("UPDATE {quiz_questions} SET question_status = %d WHERE quiz_nid = %d AND question_nid = %d", $value, $quiz->nid, $key); - if (!$result) { - $return = false; - } - } - else { - $result = db_query("INSERT INTO {quiz_questions} (quiz_nid, question_nid, question_status) VALUES (%d, %d, %d)", $quiz->nid, $key, $value); - if (!$result) { - $return = false; - } - } + db_query("INSERT INTO {quiz_questions} (quiz_nid, question_nid, question_priority) VALUES (%d, %d, %d)", $quiz->nid, $question_nid, $priority); } } - - } - else { - $return = false; - } - - return $return; - -} - - -/** - * Implementation of hook_settings() - */ -function quiz_admin_settings() { - $form = array(); - // option to globally turn off pass / fail form elements - $form['quiz_default_close'] = array( - '#type' => 'textfield', - '#title' => t('Default number of days before a @quiz is closed', array('@quiz' => QUIZ_NAME)), - '#default_value' => variable_get('quiz_default_close', 30), - '#description' => t('Supply a number of days to calculate the default close date for new quizzes.'), - ); - $form['quiz_use_passfail'] = array( - '#type' => 'checkbox', - '#title' => t('Display pass / fail options in the @quiz form', array('@quiz' => QUIZ_NAME)), - '#default_value' => variable_get('quiz_use_passfail', 1), - '#description' => t('Check this to display the pass / fail options in the quiz form. You can still choose to ignore pass / fail options on a quiz by quiz basis if this is checked, but if you want to prohibit other quiz module creators from using these options, unchecked this option.'), - ); - $form['quiz_default_pass_rate'] = array( - '#type' => 'textfield', - '#title' => t('Default percentage needed to pass a @quiz', array('@quiz' => QUIZ_NAME)), - '#default_value' => variable_get('quiz_default_pass_rate', 75), - '#description' => t('Supply a number between 1 and 100 to set as the default percentage correct needed to pass a quiz. Set to 0 if you want to ignore pass / fail summary information by default.'), - ); - $form['quiz_name'] = array( - '#type' => 'textfield', - '#title' => t('Assessment name'), - '#default_value' => QUIZ_NAME, - '#description' => t('How do you want to refer to quizzes across the site (for example: quiz, test, assessment). This will affect display text but will not affect menu paths.'), - '#required' => TRUE, - ); - return system_settings_form($form); -} - -/** - * Validation form settings form - */ -function quiz_settings_form_validate($form_id, $form_values) { - if (!is_numeric($form_values['quiz_default_close']) || $form_values['quiz_default_close'] <= 0) { - form_set_error('quiz_default_close', t('The default number of days before a quiz is closed must be a number greater than 0.')); - } - if (!is_numeric($form_values['quiz_default_pass_rate'])) { - form_set_error('quiz_default_pass_rate', t('The pass rate value must be a number between 0% and 100%.')); - } - if ($form_values['quiz_default_pass_rate'] > 100) { - form_set_error('quiz_default_pass_rate', t('The pass rate value must not be more than 100%.')); - } - if ($form_values['quiz_default_pass_rate'] < 0) { - form_set_error('quiz_default_pass_rate', t('The pass rate value must not be less than 0%.')); } } /** - * Quiz Admin - * - */ -function quiz_admin() { - $results = _quiz_get_results(); - return theme('quiz_admin', $results); -} - - -/* - * Get a full results list + * Get a full list of all the quizzes taken along with pertinent details. + * + * @param $nid + * The node ID of the quiz. + * @param $uid + * An optional user ID used to restrict the result set to a particular user. + * @param $pager + * Boolean: Controls if the results are to be paged. + * + * @return $results + * An array of results. */ -function _quiz_get_results($nid = '', $uid = 0) { +function _quiz_get_results($nid = NULL, $uid = 0, $pager = TRUE) { $results = array(); $args = array(); - $sql = "SELECT n.nid nid, - n.title title, - u.name name, - qr.rid rid, - qr.time_start, - qr.time_end - FROM {node} n, {quiz} q, {quiz_result} qr, {users} u - WHERE - n.type = 'quiz' - AND - n.nid = q.nid - AND - qr.quiz_nid = q.nid - AND - u.uid = qr.uid"; + $sql = "SELECT n.nid, n.title, u.uid, u.name, qr.rid, qr.time_start, qr.time_end, qr.score + FROM {node} n INNER JOIN {quiz} q ON (q.nid = n.nid) INNER JOIN {quiz_results} qr ON (qr.quiz_nid = q.nid) INNER JOIN {users} u ON (u.uid = qr.uid) + WHERE n.type = 'quiz' + "; if ($nid) { $sql .= " AND qr.quiz_nid = %d"; $args[] = $nid; @@ -1373,484 +1469,577 @@ $sql .= " AND qr.uid = %d"; $args[] = $uid; } - $sql .= " ORDER BY qr.rid ASC"; - $dbresult = db_query($sql, $args); - //Create results array - while ($line = db_fetch_array($dbresult)) { - $results[$line['rid']] = $line; - } - return $results; -} - -/* - * Quiz Results User - */ -function quiz_user_results() { - $result = db_fetch_object(db_query('SELECT q.nid FROM {quiz} q, {quiz_result} qr WHERE qr.quiz_nid = q.nid AND qr.rid = %d', arg(2))); - if ($result->nid) { - $quiz = node_load($result->nid); - $questions = _quiz_get_answers(arg(2)); - $score = quiz_calculate_score(arg(2)); - $summary = _quiz_get_summary_text($quiz, $score); - return theme('quiz_user_summary', $quiz, $questions, $score, $summary); + $sql .= " ORDER BY qr.rid DESC"; + if ($pager) { + $result = pager_query($sql, quiz_variable_get('pager'), 0, NULL, $args); } else { - drupal_not_found(); + $result = db_query($sql, $args); } -} -/* - * Quiz Results Admin - */ -function quiz_admin_results() { - $result = db_fetch_object(db_query('SELECT quiz.nid FROM {quiz} quiz, {quiz_result} quiz_result WHERE quiz_result.quiz_nid = quiz.nid AND quiz_result.rid = %d', arg(2))); - if ($result->nid) { - $quiz = node_load($result->nid); - $questions = _quiz_get_answers(arg(2)); - $score = quiz_calculate_score(arg(2)); - $summary = _quiz_get_summary_text($quiz, $score); - return theme('quiz_admin_summary', $quiz, $questions, $score, $summary); - } - else { - drupal_not_found(); + // Create results array. + while ($score = db_fetch_object($result)) { + $results[$score->rid] = $score; } -} + return $results; +} -/* - * Delete Result +/** + * Retrieve stored results for a quiz or a particular question in a quiz. + * + * @param $rid + * The result ID of the quiz. + * @param $qid + * The optional question ID of the question whose results are to be retrieved. + * + * @return $return + * Stored results in array form. */ -function quiz_admin_result_delete() { - return drupal_get_form('quiz_admin_result_delete_form'); -} +function _quiz_get_answers($rid, $qid = NULL) { + $results = array(); -function quiz_admin_result_delete_form() { - $form['del_rid'] = array('#type' => 'hidden', '#value' => arg(2)); - return confirm_form($form, - t('Are you sure you want to delete this @quiz result?', array('@quiz' => QUIZ_NAME)), - 'admin/quiz', - t('This action cannot be undone.'), - t('Delete'), - t('Cancel')); + // Perform JOINs on revision ID to ensure that the results reflect the + // question as at the time of taking the quiz. + $sql = "SELECT qqr.question_nid, qqr.question_vid, qqr.tries + FROM {quiz_question_results} qqr INNER JOIN {quiz_question} qq ON (qqr.question_vid = qq.vid AND qqr.question_nid = qq.nid) + WHERE qqr.rid = %d"; + $args[] = $rid; + + // Retrive only a specific result if $qid is valid. + if ($qid) { + $sql .= " AND qq.nid = %d"; + $args[] = $qid; + } + + $result = db_query($sql, $args); + + // Create results array. + while ($line = db_fetch_array($result)) { + if ($question = quiz_load_revision($line['question_nid'], $line['question_vid'])) { + $results[$line['question_nid']] = array('question' => $question, 'tries' => unserialize($line['tries'])); + } + } + + return $results; } -function quiz_admin_result_delete_form_submit($form_id, $form_values) { - db_query("DELETE FROM {quiz_result} WHERE rid = %d", $form_values['del_rid']); - db_query("DELETE FROM {quiz_question_results} WHERE result_rid = %d", $form_values['del_rid']); - drupal_set_message(t('Deleted result.')); +/** + * Check if a quiz has more questions set to be 'always asked' than the maximum + * number of questions property of the quiz. + * + * @param $quiz + * The quiz node object. + * @return $return + * TRUE if the question count is satisfactory. Else, the estimated question + * count is returned. + */ +function quiz_validate_max_questions($quiz) { + // 0 implies unlimited questions. + if ($quiz->number_of_questions > 0) { + $min_questions = quiz_get_num_questions($quiz->nid, QUESTION_ALWAYS); + + // If there are too many questions set to "always", increase the number of + // questions. + if ($min_questions > $quiz->number_of_questions) { + return $min_questions; + } + } - return "admin/quiz"; + return TRUE; } +/** + * Check if a quiz is available. + * + * @param $node + * The quiz node object. + * @return $return + * TRUE if the quiz is available or an appropriate error message if not. + */ +function quiz_validate_availability($node) { + $return = TRUE; -function _quiz_get_answers($rid) { - $results = array(); - $dbresult = db_query("SELECT - qqr.question_nid qnid, - qqr.answer qanswer, - nr.body question, - n.type type - FROM {quiz_question_results} qqr, {quiz_question} qq, {node} n, {node_revisions} nr - WHERE - qqr.result_rid = %d - AND - qqr.question_nid = qq.nid - AND - n.nid = qq.nid - AND - nr.nid = n.nid", $rid); - //Create results array - while ($line = db_fetch_array($dbresult)) { - $results[$line['qnid']] = $line; + if (!$node->quiz_always) { + $time = time(); + if ($node->quiz_close <= $time) { + $return = t('This @quiz is no longer available.', array('@quiz' => QUIZ_NAME)); + } + else if ($node->quiz_open > $time) { + if (is_array($node->quiz_close)) { + quiz_translate_form_date($node, 'quiz_close'); + } + + $return = t('This @quiz will be available from %date.', array('@quiz' => QUIZ_NAME, '%date' => format_date($node->quiz_close))); + } } - return $results; + + return $return; } /** - * Get the quiz name variable for use as a constant - * so we don't have to keep calling for it in every function + * Check if a user has reached the maximum number of takes available. * - * @return - * quiz name variable - **/ -function _quiz_get_quiz_name() { - return variable_get('quiz_name', 'Quiz'); -} + * @param $quiz + * The quiz node object. + * @param $uid + * User ID. + * @return $return + * TRUE if takes are available or an appropriate error message if not. + */ +function quiz_validate_takes($quiz, $uid) { + if ($uid > 0) { + $takes = db_result(db_query('SELECT COUNT(rid) AS count FROM {quiz_results} WHERE uid = %d AND quiz_nid = %d', $uid, $quiz->nid)); + // FIXME: Allow the quiz owner and quiz admins to retake the quiz as many + // times as they like. + if ($quiz->takes != 0 && $takes >= $quiz->takes) { + return t('You have already taken this @quiz the maximum %d !times.', array('@quiz' => QUIZ_NAME, '!times' => format_plural($quiz->takes, t('time'), t('times')), '%d' => $quiz->takes)); + } + } + return TRUE; +} ///////////////////////////////////////////////// -/// Theme functions +// Theme functions ///////////////////////////////////////////////// /** - * Theme the admin results table - * + * Theme the admin results table. + * + * @todo Perhaps club this with the user results theme function. + * * @param $results - * As returned by _quiz_get_results() + * As returned by _quiz_get_results(). + * + * @return + * Themed HTML. */ function theme_quiz_admin($results) { - $output = ''; $rows = array(); - while (list($key, $result) = each($results)) { + foreach ($results as $result) { + if ($result->time_end > 0) { + $finished = format_date($result->time_end, 'small'); + $view = l('View', 'admin/quiz/'. $result->rid); + } + else { + $finished = t('In progress'); + $view = t('View'); + } $rows[] = array( - l('view', 'admin/quiz/'. $result['rid'] .'/view') .' | '. l('delete', 'admin/quiz/'. $result['rid'] .'/delete'), - check_plain($result['title']), - check_plain($result['name']), - $result['rid'], - format_date($result['time_start'], 'small'), - ($result['time_end'] > 0) ? format_date($result['time_end'], 'small') : t('In Progress'), + l($result->title, 'node/'. $result->nid), + theme('username', $result), + t('@score%', array('@score' => $result->score)), + format_date($result->time_start, 'small'), + $finished, + $view, + l('Delete', 'admin/quiz/'. $result->rid .'/delete') ); } - + $header = array( - t('Action'), - t('@quiz Title', array('@quiz' => QUIZ_NAME)), + t('@quiz title', array('@quiz' => QUIZ_NAME)), t('Username'), - t('Result
ID'), - t('Time Started'), - t('Finished?')); - + t('Score'), + t('Time started'), + t('Finished'), + array('data' => t('Operations'), 'colspan' => 2) + ); + if (!empty($rows)) { - $output .= theme('table', $header, $rows); + $output = theme('table', $header, $rows); + $output .= theme('pager', NULL, quiz_variable_get('pager'), 0); } else { - $output .= t('No @quiz results found.', array('@quiz' => QUIZ_NAME)); + $output = t('No @quiz results found.', array('@quiz' => QUIZ_NAME)); } + return $output; } /** - * Theme the user results page - * + * Theme the user results page. + * * @param $results - * An array of quiz information + * An array of quiz information. * @return - * Themed html + * Themed HTML. */ function theme_quiz_get_user_results($results) { - $output = ''; $rows = array(); - while (list($key, $result) = each($results)) { + + foreach ($results as $result) { + if ($result->time_end > 0) { + $finished = format_date($result->time_end, 'small'); + $view = l('View', 'user/quiz/'. $result->rid); + } + else { + $finished = t('In progress'); + $view = t('View'); + } $rows[] = array( - l('view', 'user/quiz/'. $result['rid'] .'/userresults'), - check_plain($result['title']), - check_plain($result['name']), - $result['rid'], - format_date($result['time_start'], 'small'), - ($result['time_end'] > 0) ? format_date($result['time_end'], 'small') : t('In Progress'), + l($result->title, 'node/'. $result->nid), + t('@score%', array('@score' => $result->score)), + format_date($result->time_start, 'small'), + $finished, + $view ); } - + $header = array( - t('Action'), t('@quiz Title', array('@quiz' => QUIZ_NAME)), - t('Username'), - t('Result
ID'), - t('Time Started'), - t('Finished?')); - + t('Score'), + t('Time started'), + t('Finished'), + t('Operations') + ); + if (!empty($rows)) { - $output .= theme('table', $header, $rows); + $output = theme('table', $header, $rows); + $output .= theme('pager', NULL, quiz_variable_get('pager'), 0); } else { - $output .= t('No @quiz results found.', array('@quiz' => QUIZ_NAME)); + $output = t('No @quiz results found.', array('@quiz' => QUIZ_NAME)); } - return $output; + + return $output; } /** - * Theme the filtered question list + * Theme the filtered question list. */ -function theme_quiz_filtered_questions($form) { - $header = array(t('Random'), t('Always'), t('Never'), t('Question'), t('Type')); +function theme_quiz_manage_questions_form($form) { $rows = array(); - while (list($nid, $values) = each($form['question_status'])) { - if (is_numeric($nid)) { - $rows[] = array( - drupal_render($form['question_status'][$nid][0]), - drupal_render($form['question_status'][$nid][1]), - drupal_render($form['question_status'][$nid][2]), - drupal_render($form['question'][$nid]), - drupal_render($form['type'][$nid]) - ); + if (isset($form['filtered_question_list']['question_priority'])) { + foreach ($form['filtered_question_list']['question_priority'] as $nid => $values) { + if (is_numeric($nid)) { + $rows[] = array( + drupal_render($form['filtered_question_list']['nodes'][$nid]), + drupal_render($form['filtered_question_list']['question'][$nid]), + drupal_render($form['filtered_question_list']['type'][$nid]), + drupal_render($form['filtered_question_list']['question_priority'][$nid]), + l('Edit', 'node/'. $nid .'/edit') + ); + } } } if (!empty($rows)) { - $output .= theme('table', $header, $rows); + $header = array(theme('table_select_header_cell'), t('Question'), t('Type'), t('Frequency'), t('Operations')); + $output = theme('table', $header, $rows); + $output .= theme('pager', NULL, quiz_variable_get('pager'), 0); + // Stick table inside the fieldset. + $form['filtered_question_list']['#children'] = $output; + $output = drupal_render($form); } else { - $output .= t('No questions found.'); - } - return $output; -} - - -/** - * Theme a table containing array of questions and options - * - * @param $questions - * Array of question nodes - * @return - * HTML to output table - */ -function theme_quiz_question_table($questions) { - $output = ''; - $rows = array(); - $status_descriptions = array(t('Random'), t('Always'), t('Never')); - while (list($key, $question) = each($questions)) { - $rows[] = array( - $status_descriptions[$question->status], - check_markup($question->question), - $question->type, - l('Edit', 'node/'. $question->nid .'/edit')); + $output = t('No questions found.'); } - $header = array(t('Status'), t('Question'), t('Type'), t('Edit')); - if (!empty($rows)) { - $output .= theme('table', $header, $rows); - } - else { - $output .= 'No questions found.'; - } return $output; } - -/** - * Pass the correct mark to the theme so that theme authors can use an image - * - * TODO: A default image might be better here. - */ -function theme_quiz_score_correct() { - return theme('image', drupal_get_path('module', 'quiz') .'/images/correct.gif', t('correct')); -} - - /** - * Pass the incorrect mark to the theme so that theme authors can use an image + * Theme a progress indicator for use during a quiz. + * + * @param $question_number + * The position of the current question in the sessions' array. + * @param $num_of_questions + * The number of questions for this quiz. * - * TODO: A default image might be better here. + * @return + * Themed HTML. */ -function theme_quiz_score_incorrect() { - return theme('image', drupal_get_path('module', 'quiz') .'/images/incorrect.gif', t('incorrect')); -} +function theme_quiz_progress($question_number, $num_of_questions) { + // Get the current question number by adding one. + $current_question = $question_number + 1; + $output = '
'; + $output .= t('Question %x of %y', array('%x' => $current_question, '%y' => $num_of_questions)); + $output .= '
'; -/** - * Theme a progress indicator for use during a quiz - * - * @param $question_number - * The position of the current question in the sessions' array - * @param $num_of_question - * The number of questions for this quiz as returned by quiz_get_number_of_questions() - * @return - * Themed html - */ -function theme_quiz_progress($question_number, $num_of_question) { - - // Determine the percentage finished (not used but left for other implementations) - //$progress = ($question_number*100)/$num_of_question; - - // Get the current question # by adding one - $current_question = $question_number + 1; - - // return html - $output = ''; - $output .= '
'; - $output .= t('Question %x of %y', array('%x' => $current_question, '%y' => $num_of_question)); - $output .= '

'."\n"; return $output; - } - /** - * Theme a question page - * + * Theme a question page. + * * @param $quiz - * The quiz node object + * The quiz node object. * @param $question_node - * The question node + * The question node. * @return - * Themed html + * Themed HTML. */ function theme_quiz_take_question($quiz, $question_node) { + // Calculation for quiz progress bar. + $question_number = $quiz->max_questions - count($_SESSION['quiz_'. $quiz->nid]['quiz_questions']); + + // Set the title here in case themers want to do something different. + drupal_set_title(check_plain($quiz->title)); + + // Return the elements of the page. + $output = theme('quiz_progress', $question_number, $quiz->max_questions); + // Render question form. Pass the Quiz nid to pass as #type => value. + $output .= module_invoke($question_node->type, 'render_question', $question_node, $quiz->nid); - //Calculation for quiz progress bar - $number_of_questions = quiz_get_number_of_questions($quiz->nid); - $question_number = $number_of_questions - count($_SESSION['quiz_'. $quiz->nid]['quiz_questions']); - - // Set the title here in case themers want to do something different - drupal_set_title(check_plain($quiz->title)); - - // Return the elements of the page - $output = ''; - $output .= theme('quiz_progress', $question_number, $number_of_questions); - $output .= module_invoke($question_node->type, 'render_question', $question_node); return $output; } - /** - * Theme the summary page after the quiz has been completed - * + * Theme the summary page after the quiz has been completed. + * * @param $quiz - * The quiz node object + * The quiz node object. * @param $questions - * The questions array as defined by _quiz_get_answers + * The questions array as defined by _quiz_get_answers. * @param $score - * Array of score information as returned by quiz_calculate_score() + * Array of score information as returned by quiz_calculate_score(). * @param $summary - * Filtered text of the summary + * Filtered text of the summary. * @return - * Themed html + * Themed HTML. */ function theme_quiz_take_summary($quiz, $questions, $score, $summary) { + global $user; - // Set the title here so themers can adjust + // Set the title here so themers can adjust. drupal_set_title(check_plain($quiz->title)); - // Display overall result - $output = ''; - $output .= '
'. t('You got %num_correct of %question_count correct.', array('%num_correct' => $score['num_correct'], '%question_count' => $score['question_count'])) .'
'."\n"; - $output .= '
'. t('Your score: @score%', array('@score' => $score['percentage_score'])) .'

'."\n"; - $output .= '
'. $summary .'

'."\n"; - - // Get the feedback for all questions - $output .= theme('quiz_feedback', $questions, FALSE, TRUE); + // Display overall result. + $output = '
'. t('You got %num_correct of %question_count correct.', array('%num_correct' => $score['num_correct'], '%question_count' => $score['question_count'])) .'
'; + $output .= '
'. t('Your score: @score%', array('@score' => $score['percentage_score'])) .'
'; + $output .= '
'. t('Pass percentage for this @quiz: @score%', array('@quiz' => QUIZ_NAME, '@score' => $quiz->pass_rate)) .'
'; + $output .= '
'. $summary .'
'; + + // Display feedback for all questions. + if ($quiz->feedback_time == FEEDBACK_AFTER_QUIZ || $quiz->feedback_time == FEEDBACK_AFTER_BOTH) { + // FIXME: This should probably be paginated. + $output .= theme('quiz_feedback', $questions, FALSE, TRUE); + } - return $output; + if (quiz_validate_takes($quiz, $user->uid) === TRUE) { + $output .= '
'. t('Retake @quiz', array('@quiz' => QUIZ_NAME, '!url' => url('node/'. $quiz->nid .'/engine'))) .'
'; + } + return $output; } - /** - * Theme the summary page for admins - * + * Theme the summary page for admins. + * * @param $quiz - * The quiz node object + * The quiz node object. * @param $questions - * The questions array as defined by _quiz_get_answers + * The questions array as defined by _quiz_get_answers. * @param $score - * Array of score information as returned by quiz_calculate_score() + * Array of score information as returned by quiz_calculate_score(). * @param $summary - * Filtered text of the summary + * Filtered text of the summary. * @return - * Themed html + * Themed HTML. */ function theme_quiz_admin_summary($quiz, $questions, $score, $summary) { - - // Set the title here so themers can adjust + // Set the title here so themers can adjust. drupal_set_title(check_plain($quiz->title)); - // Display overall result - $output = ''; - $output .= '
'. t('This person got %num_correct of %question_count correct.', array('%num_correct' => $score['num_correct'], '%question_count' => $score['question_count'])) .'
'."\n"; - $output .= '
'. t('Total score: @score%', array('@score' => $score['percentage_score'])) .'

'."\n"; - $output .= '
'. $summary .'

'."\n"; - - // Get the feedback for all questions + // Display overall result. + $output = '
'. t('User got %num_correct of %question_count correct.', array('%num_correct' => $score['num_correct'], '%question_count' => $score['question_count'])) .'
'; + $output .= '
'. t('Total score: @score%', array('@score' => $score['percentage_score'])) .'
'; + $output .= '
'. $summary .'
'; + + // Get the feedback for all questions. $output .= theme('quiz_feedback', $questions, TRUE, TRUE); return $output; } - /** - * Theme the summary page for user results - * + * Theme the summary page for user results. + * * @param $quiz - * The quiz node object + * The quiz node object. * @param $questions - * The questions array as defined by _quiz_get_answers + * The questions array as defined by _quiz_get_answers. * @param $score - * Array of score information as returned by quiz_calculate_score() + * Array of score information as returned by quiz_calculate_score(). * @param $summary - * Filtered text of the summary + * Filtered text of the summary. * @return - * Themed html + * Themed HTML. */ function theme_quiz_user_summary($quiz, $questions, $score, $summary) { - - // Set the title here so themers can adjust + // Set the title here so themers can adjust. drupal_set_title(check_plain($quiz->title)); - // Display overall result - $output = ''; - $output .= '
'. t('You got %num_correct of %question_count correct.', array('%num_correct' => $score['num_correct'], '%question_count' => $score['question_count'])) .'
'."\n"; - $output .= '
'. t('Your score was: @score%', array('@score' => $score['percentage_score'])) .'

'."\n"; - $output .= '
'. $summary .'

'."\n"; - - // Get the feedback for all questions + // Display overall result. + $output = '
'. t('You got %num_correct of %question_count correct.', array('%num_correct' => $score['num_correct'], '%question_count' => $score['question_count'])) .'
'; + $output .= '
'. t('Your score was: @score%', array('@score' => $score['percentage_score'])) .'
'; + $output .= '
'. $summary .'
'; + + // Get the feedback for all questions. Correct answers are not shown. $output .= theme('quiz_feedback', $questions, FALSE, TRUE); return $output; - } - /** - * Theme the question feedback - * + * Theme the question feedback. + * * @param $questions - * Array of quiz objects as returned by _quiz_get_answers - * @param showpoints - * Binary flag for whether to show the actual answers - * @param $showfeedback - * binary flag for whether to show question feedback + * Array of quiz objects as returned by _quiz_get_answers. + * @param $show_answers + * Binary flag for whether to show the actual answers. + * @param $show_feedback + * Binary flag for whether to show question feedback. * @return - * Themed html + * Themed HTML. */ -function theme_quiz_feedback($questions, $showpoints = TRUE, $showfeedback = FALSE) { +function theme_quiz_feedback($questions, $show_answers = TRUE, $show_feedback = FALSE) { $rows = array(); - $header = array(t('Question Result(s)'), ''); - // go through each of the questions - while (list($key, $question) = each($questions)) { - - // reset the cols array + $header = array(t('Question result(s)')); + // Go through each of the questions. + foreach ($questions as $question) { + // Reset the cols array. $cols = array(); - - // Get the answer table for this question - $question['qanswer'] = unserialize($question['qanswer']); - $result = module_invoke($question['type'], 'calculate_results', $question['qanswer']['answers'], $question['qanswer']['tried'], $showpoints, $showfeedback); - - // Build the question answers header (add blank space for IE) - $innerheader = array(t('Answers')); - if ($showpoints) { - $innerheader[] = t('Correct Answer'); - } - $innerheader[] = t('User Answer'); - if ($showfeedback) { - $innerheader[] = ' '; - } - - // Add the cell with the question and the answers - $q_output = '
Q: '. check_markup($question['question']) .'
'; - $q_output .= theme('table', $innerheader, $result['resultstable']) .'
'; - $cols[] = array('data' => $q_output, 'class' => 'quiz_summary_qcell'); - - // Get the score result for each question. - if ($result['score'] == 1) { - $cols[] = array('data' => theme('quiz_score_correct'), 'class' => 'quiz_summary_qcell'); - } - else { - $cols[] = array('data' => theme('quiz_score_incorrect'), 'class' => 'quiz_summary_qcell'); - } - // pack all of this into this row - $rows[] = array('data' => $cols, 'class' => 'quiz_summary_qrow'); + // Get the answer table for this question. + $output = theme('quiz_result_feedback', $question, $show_answers, $show_feedback); + $cols[] = array('data' => $output, 'class' => 'quiz-summary-qcell'); + + // Pack all of this into this row. + $rows[] = array('data' => $cols, 'class' => 'quiz-summary-qrow'); } + return theme('table', $header, $rows); } - /** - * Allow the option to theme the questions form + * Theme the feedback page for a single question. + * + * @param $quiz + * The quiz node. + * @param $question + * The question node. + * @param $rid + * Result ID used to display the answers table. + * + * @return + * Themed HTML. */ -function theme_quiz_questions($form) { - $output = ''; - $output .= drupal_render($form); +function theme_quiz_question_feedback($quiz, $question, $rid) { + // Set page title. + drupal_set_title(check_plain($quiz->title)); + + $question_number = $quiz->max_questions - count($_SESSION['quiz_'. $quiz->nid]['quiz_questions']) - 1; + + $output = theme('quiz_progress', $question_number, $quiz->max_questions); + + $result = _quiz_get_answers($rid, $question->nid); + $result = array_shift($result); + + $output .= theme('quiz_result_feedback', $result, TRUE, TRUE); + + if (!empty($question->explanation)) { + $output .= '
'. check_markup($question->explanation, $question->explanation_format, FALSE) .'
'; + } + + $output .= '
'. l('Continue', 'node/'. $quiz->nid .'/engine') .'
'; + + return $output; +} + +/** + * Theme the question feedback. + * + * @param $question_result + * Question array as returned by _quiz_get_answers. + * @param $show_answer + * Binary flag for whether to show the actual answers. + * @param $show_feedback + * Binary flag for whether to show question feedback. + * @return + * Themed HTML. + */ +function theme_quiz_result_feedback($question_result, $show_answer = TRUE, $show_feedback = FALSE) { + $question = $question_result['question']; + $result = module_invoke($question->type, 'calculate_results', $question, $question_result['tries'], $show_answer, $show_feedback); + + // Build the question answers header. + $header = array('', t('Answers')); + if ($show_answer) { + $header[] = t('Correct answer'); + } + $header[] = t('User answer'); + if ($show_feedback) { + $header[] = t('Feedback'); + } + + // Add the cell with the question and the answers. + $output = '
'. check_markup($question->body, $question->format, FALSE) .'
'; + + $row_span = count($result['results_table']); + // Get the score result for each question. + if ($result['score'] == 1) { + $score = array('rowspan' => $row_span, 'class' => 'quiz-score-correct', 'title' => t('Correct')); + } + else { + $score = array('rowspan' => $row_span, 'class' => 'quiz-score-incorrect', 'title' => t('Incorrect')); + } + + array_unshift($result['results_table'][0], $score); + $output .= theme('table', $header, $result['results_table']); + return $output; } + +/** + * Theme the node view for quizzes. + */ +function theme_quiz_view($node, $teaser = FALSE, $page = FALSE) { + $row_header = array( + t('Max questions'), + t('Pass percentage'), + t('Maximum takes') + ); + $rows[] = array($node->number_of_questions ? check_plain($node->number_of_questions) : t('All')); + $rows[] = array($node->pass_rate ? check_plain($node->pass_rate) : t('N/A')); + $rows[] = array($node->takes ? check_plain($node->takes) : t('Unlimited')); + + if (!$node->quiz_always) { + if (is_array($node->quiz_close)) { + quiz_translate_form_date($node, 'quiz_close'); + } + + $row_header[] = t('Close date'); + $rows[] = array(format_date($node->quiz_close)); + } + + return theme('quiz_row_table', array(), $row_header, $rows, array('class' => 'quiz-options-table'), t('@quiz options', array('@quiz' => QUIZ_NAME))); +} + +/** + * Return a themed table with row headers. + * + * @param $column_header + * An array containing the table column headers. + * @param $row_header + * An array containing the table row headers. + * @param $rows + * An array of table rows. + * @param $attributes + * An array of HTML attributes to apply to the table tag. + * @param $caption + * A localised string to use for the tag. + * @return + * An HTML string representing the table. + */ +function theme_quiz_row_table($column_header, $row_header, $rows, $attributes = array(), $caption = NULL) { + foreach ($rows as $number => $row) { + array_unshift($row, array('data' => $row_header[$number], 'header' => TRUE)); + $rows[$number] = $row; + } + + return theme('table', $column_header, $rows, $attributes, $caption); +} Index: quiz.css =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/quiz/quiz.css,v retrieving revision 1.4.2.1 diff -u -r1.4.2.1 quiz.css --- quiz.css 16 May 2007 18:22:02 -0000 1.4.2.1 +++ quiz.css 21 Jun 2007 09:11:00 -0000 @@ -1,55 +1,57 @@ -/* $Id: quiz.css,v 1.4.2.1 2007/05/16 18:22:02 add1sun Exp $ */ +/* $Id: quiz.css 113 2007-06-13 05:33:31Z karthik $ */ -/* -** Definitions that apply while taking the quiz -*/ -#quiz_progress { +.quiz-options-table { + width: auto; +} + +#quiz-progress { font-style: italic; font-size: 80%; } -div.multichoice_answer_text p { - display: inline; +.quiz-question-bullet { + font-weight: bold; + font-size: 120%; +} + +#quiz-score-possible, #quiz-score-percent { + font-weight: bold; } -.multichoice_answer_text { - margin: -1.9em 0 0 2em; +.quiz-score-correct, .quiz-score-incorrect { + width: 50px; } -/* -** Definitions that apply on the summary pages -*/ -.quiz_question_bullet { - font-weight: bold; - font-size: 120%; +.quiz-score-correct { + background: transparent url(images/correct.gif) no-repeat center; } -#quiz_score_possible, #quiz_score_percent { - font-weight: bold; +.quiz-score-incorrect { + background: transparent url(images/incorrect.gif) no-repeat center; } -.quiz_summary_question { +.quiz-summary-question { margin-bottom: 0.5em; } -tr.quiz_summary_qrow { +tr.quiz-summary-qrow { background: transparent; } -td.quiz_summary_qcell { +td.quiz-summary-qcell { vertical-align: top; padding: 1em 1em 0em 0em; } -td.quiz_summary_qcell table tr { +td.quiz-summary-qcell table tr { background: transparent; } -td.quiz_summary_qcell table td { +td.quiz-summary-qcell table td { vertical-align: top; padding: .5em; } -.quiz_answer_feedback { +.quiz-answer-feedback { font-style: italic; } Index: multichoice.install =================================================================== RCS file: multichoice.install diff -N multichoice.install --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ multichoice.install 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,46 @@ + 'nid', + '{quiz_question}' => 'nid', + '{quiz_questions}' => 'question_nid', + '{quiz_question_answer}' => 'question_nid', + '{quiz_question_results}' => 'question_nid' + ); + + $result = db_query("SELECT nid FROM {node} WHERE type = 'multichoice'"); + while ($node = db_fetch_array($result)) { + _multichoice_purge_database($table_field_array, $node['nid']); + } + db_query("DELETE FROM {node} WHERE type = 'multichoice'"); + + drupal_set_message(t('Multichoice module: Uninstallation script complete.')); +} + +/** + * Helper function for the uninstallation script. + * + * @param $table_field_array + * Associative array of table and field names. + * @param $value + * The argument for the field which is to be deleted from the table. + * @param $type + * The type of the argument: %d, '%s' etc. + */ +function _multichoice_purge_database($table_field_array, $value, $type = '%d') { + foreach ($table_field_array as $table => $field) { + db_query("DELETE FROM $table WHERE $field = $type", $value); + } +}