--- cvs/multichoice.module 2006-11-01 02:04:35.000000000 +0100 +++ local/multichoice.module 2006-11-08 21:09:21.000000000 +0100 @@ -1,5 +1,5 @@ array('name' => t('multichoice'), 'base' => 'multichoice')); +/// prefix quiz parts (multichoice, matching, fillblank, etc.) with 'quiz -' +/// so we remember that those node type are not independant, but part of a quiz. +/// in another hand, they are grouped, what ease content addition in the jungle.. + return array('multichoice' => array('name' => t('quiz - Multiple choice'), 'base' => 'multichoice')); } /** @@ -46,7 +49,8 @@ function multichoice_menu($may_cache) { $items = array(); if ($may_cache) { $items[] = array('path' => 'node/add/multichoice', - 'title' => t('multichoice'), +/// prefix quiz question type with "quiz - "... + 'title' => t('quiz - Multiple choice'), 'access' => user_access('create multichoice')); } return $items; @@ -74,10 +78,11 @@ function multichoice_form(&$node) { ); $form['body_filter']['format'] = filter_form($node->format); - $form['multiple_answers'] = array( - '#type' => 'checkbox', - '#title' => t('Multiple answers'), - '#default_value' => $node->multiple_answers, + $form['properties'] = array( + '#type' => 'fieldset', + '#title' => t('Settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, ); // Determine number of answer rows to display @@ -87,6 +92,69 @@ function multichoice_form(&$node) { if ($_POST['edit']['more']) { $node->rows += 5; } + + // drop-down menu options-head + $multiple_max_list = array( + 0 => t(' Unique answer'), + 1 => t('Multiple answers'), + ); + // drop-down menu options-tail + for ($i = 2; $i <= $node->rows; $i++) { + $multiple_max_list[$i] = t('- up to %maxCount answers', array('%maxCount' => $i)); + } + // drop-down menu form + $form['properties']['multiple_answers'] = array( + '#type' => 'select', + '#title' => t('answers'), + '#default_value' => (isset($node->multiple_answers) ? $node->multiple_answers : 0), + '#options' => $multiple_max_list, + '#description' => t('How many answers are needed for that question?'), + ); + + // prevent from automatic click without thinking about the answer: +/// this may happen when you take the same quiz too often but do not +/// understand the feedback (then just click the position known to be the right +/// one, but don't know why)... sure one can simply memorise the full answer, +/// but it may be harder, especialy when the answers look similar... + $form['properties']['shuffled_answers'] = array( + '#type' => 'checkbox', + '#title' => t('shuffle answers order'), + '#default_value' => (isset($node->shuffled_answers) ? $node->shuffled_answers : 0), + '#description' => t('Check that box if the answers should be displayed in a random order.'), + ); + + // use inline drop-down list when answers are short (often one word) +/// question: choose the right tense for this sentense +/// when he !l! in, we were still playing +/// answers: 1) came +/// 2) comes +/// 3) will come +/// a selection list is shown here at the location marked !l! ;) + $form['properties']['printable_answers'] = array( + '#type' => 'select', + '#title' => t('display'), + '#default_value' => (isset($node->printable_answers) ? $node->printable_answers : check_plain(variable_get('quiz_default_answer_displaying', 1))), + '#options' => array(0 => t('web-only/compact'), 1 => t('printable/full'), ), + '#description' => t('Do you want all the answers visible at the same time?'), + ); + + // use of automatic basic scoring or advanced personalized one +/// this allow the mix of both kind of scoring in a quiz.. thanks Sean. +/// here we ovewrite a global setting... Of course, quiz_settings() must be updated with a section like : +/// $form['quiz_rich_scoring'] = array ( +/// '#type' => 'checkbox', +/// '#title' => t('Use a complex scoring'), +/// '#default_value' => variable_get('quiz_default_answer_scoring', 0), +/// ''#description' => t('Check this if you want to set your own score (from -3 to 5) for each answer.', +/// ); + $form['properties']['extend_scoring'] = array( + '#type' => 'select', + '#title' => t('scoring'), + '#default_value' => (isset($node->extend_scoring) ? $node->extend_scoring : check_plain(variable_get('quiz_default_answer_scoring', 0))), + '#options' => array(0 => t('automatic {0}||{1}'), 1 => t('personalized {-3, -2, -1, 0}||{1, 2, 3, 4, 5}'), ), + '#description' => t('Please fill the answers and choose the correct ones, then Preview if you change the score handlling.'), + ); + $answers = $node->answers; // Display answer rows @@ -96,12 +164,29 @@ function multichoice_form(&$node) { '#tree' => TRUE, '#theme' => 'multichoice_form' ); - + + $points_list = array(); + for ($i = -3; $i < 6; $i++) { + $points_list[$i] = $i; + } for ($i = 0; $i < $node->rows; $i++) { - $form['answers'][$i]['correct'] = array( - '#type' => 'checkbox', - '#default_value' => $answers[$i]['points'], - ); + + if($node->extend_scoring) { +/// each answer can have different point(s), not only 0 || 1. +/// read clairem's example on CMS (http://drupal.org/comment/reply/15934/26603) + $form['answers'][$i]['correct'] = array( + '#type' => 'select', + '#default_value' => $answers[$i]['points'], + '#options' => $points_list, + '#description' => t('>0 if correct.'), // explain/remember how it works. + ); + } + else { + $form['answers'][$i]['correct'] = array( + '#type' => 'checkbox', + '#default_value' => $answers[$i]['points'], + ); + } $form['answers'][$i]['answer'] = array( '#type' => 'textarea', '#default_value' => $answers[$i]['answer'], @@ -128,6 +213,7 @@ function multichoice_form(&$node) { $form['more'] = array( '#type' => 'checkbox', '#title' => t('I need more answers'), + '#description' => t("If the amount of boxes above isn't enough, check this box and click the Preview button below to add some more.") .t(' Let it checked after previewing..'), ); return $form; @@ -142,7 +228,9 @@ function multichoice_validate(&$node) { $node->teaser = 0; $node->promote = 0; - if (!$node->nid && empty($_POST)) return; + if (!$node->nid && empty($_POST)) { + return; + } // Validate body if (!$node->body) { @@ -154,11 +242,7 @@ function multichoice_validate(&$node) { $corrects = 0; while(list($key, $answer) = each($node->answers)) { - - if ($answer['correct']) { - if ($corrects && !$node->multiple_answers) { - form_set_error('multiple_answers', t('Single choice yet multiple correct answers are present')); - } + if ($answer['correct'] > 0) { $corrects++; } if ($answer['answer']) { @@ -167,25 +251,39 @@ function multichoice_validate(&$node) { } if (!$corrects) { - form_set_error("answers][0]['correct'", t('No correct choice(s)')); + form_set_error("answers][0]['correct'", t('No correct choice.')); } if (!count($answers)) { form_set_error("answers][0]['answer'", t('No answers.')); } - if (count($answers) < 2) { - form_set_error("answers][0]['answer'", t('Must have at least two answers')); +/// it's not true that a multiple answers type must have at least two answers.. +/// example : the 9th question of the MySQL certification example test (available at http://www.mysql.com/certification/selftest/core/index.php) is presented like a multiple answers with the choices +/// a) INSERT INTO parents VALUES (Linda),(Alice),(John) +/// b) INSERT INTO parents VALUES ("Linda","Alice","John") +/// c) INSERT INTO parents VALUES ("Linda"),("Alice"),("John") +/// d) INSERT INTO parents VALUES "Linda","Alice", "John" +/// e) INSERT INTO parents (child, mother, father) VALUES ("John"),("Linda"),("Alice") +/// but there's only one true answer :) +/// however, the drop-down menu allow a funny trick :) + if (count($answers) < $node->multiple_answers) { + form_set_error("answers][0]['answer'", t('Must have at least %minCount answers', array('%minCount' => $node->multiple_answers))); + } + if ($correct < $node->multiple_answers) { + form_set_error("answers][0]['answer'", t('Must have %minCount correct answers', array('%minCount' => $node->multiple_answers))); } + } /** * 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))); - + db_query("INSERT INTO {quiz_question} (nid, properties) VALUES(%d, '%s')", $node->nid, serialize(array('multiple_answers' => $node->multiple_answers, 'shuffled_answers' => $node->shuffled_answers, 'printable_answers' => $node->printable_answers, 'extend_scoring' => $node->extend_scoring, ))); + while(list($key, $value) = each($node->answers)) { if (trim($value['answer']) != "") - db_query("INSERT INTO {quiz_question_answer} (question_nid, answer, feedback, points) VALUES(%d, '%s', '%s', %d)", $node->nid, $value['answer'], $value['feedback'], $value['correct']); + 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']); } } @@ -193,13 +291,14 @@ function multichoice_insert(&$node) { * 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); - + db_query("UPDATE {quiz_question} SET properties = '%s' WHERE nid = %d", serialize(array('multiple_answers' => $node->multiple_answers, 'shuffled_answers' => $node->shuffled_answers, 'printable_answers' => $node->printable_answers, 'extend_scoring' => $node->extend_scoring, )), $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 +/// beware: not only '', but 0 and '0' also are empty values for PHP +/// but $value['delete'] can be seen like a C boolean, i.e O (or unset) for false and any non-zero value if true + if (!empty($value['delete']) || trim($value['answer']) == '') { db_query("DELETE FROM {quiz_question_answer} WHERE aid = %d", $value['aid']); } else { //Update this entry @@ -207,7 +306,8 @@ function multichoice_update($node) { } } else if (trim($value['answer']) != "") { //If there is an answer, insert a new row - db_query("INSERT INTO {quiz_question_answer} (question_nid, answer, feedback, points) VALUES(%d, '%s', '%s', %d)", $node->nid, $value['answer'], $value['feedback'], $value['correct']); + 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']); } } } @@ -225,19 +325,22 @@ function multichoice_delete(&$node) { */ function multichoice_load($node) { $additions = db_fetch_object(db_query("SELECT * FROM {quiz_question} WHERE nid = %d", $node->nid)); - + $answers = array(); $result = db_query("SELECT * FROM {quiz_question_answer} WHERE question_nid = %d", $node->nid); while($line = db_fetch_array($result)) { $answers[] = $line; } - - + $additions->answers = $answers; - + $additions->properties = unserialize($additions->properties); $additions->multiple_answers = $additions->properties['multiple_answers']; - + $additions->shuffled_answers = $additions->properties['shuffled_answers']; + $additions->printable_answers = $additions->properties['printable_answers']; + $additions->extend_scoring = $additions->properties['extend_scoring']; + + return $additions; } @@ -281,34 +384,109 @@ function multichoice_help($section) { * HTML output */ function multichoice_render_question($node) { - // Radio buttons for single selection questions, checkboxes for multiselect - if ($node->multiple_answers == 0) { - $type = 'radios'; + $question = check_markup($node->body, $node->format); + $ok = eregi("![a-z]!", $question, $tag); + $prefix = '
'; // was class="multichoice_form" + $prefix .= '
'; // + /// full interface : should always be the default. + if ($node->printable_answers != 0) { + + // Radio buttons for single selection questions, checkboxes for multiselect + if (!$node->multiple_answers) { + $type = 'radios'; + } + else { + $type = 'checkboxes'; + } + + if ($ok) { + // remove the useless tag that should not be displayed + $prefix .= str_replace($tag[0], "", $question); + } + else { + $prefix .= $question; + } + $prefix .= '
'; // class="quiz_question" + $prefix .= '
'; // was class="multichoice_answer" + $suffix = '
'; // class="quiz_answer" + + } + /// compact interface : here having a default answer ("can't answer") is + /// usefull :) but this interface can be used only if choices are one-line... + else { + + // simple select for single selection questions, select multiple for multiselect + $type = 'select'; + + if ($ok) { + // get head.. + $prefix .= substr($question, 0, strpos($question, $tag[0], 0)); + } + else { + $prefix .= $question; + } + $prefix .= ''; // was class="multichoice_answer" + $suffix = ''; // class="quiz_answer" + if ($ok) { + // get tail.. + $suffix .= substr($question, strpos($question, $tag[0], 0) + 3); + } + $suffix = '
'; // class="quiz_question" + + } + + $suffix .= ''; // class="quiz_form" + if ($node->multiple_answers) { + //$desc = t('Please choose the correct answers from 1 to %maxCount', array('%maxCount' => $node->multiple_answers)); // precious hint + $desc = t('Please choose from 1 to %maxCount correct answer(s)', array('%maxCount' => count($node->answers))); } else { - $type = 'checkboxes'; + $desc = t('Please choose the right aswer'); } + // Get options $options = array(); - +//// do it here ? + if ($node->shuffled_answers) { + shuffle($node->answers); + } while(list($key, $answer) = each($node->answers)) { if (empty($answer['correct']) && empty($answer['answer']) && empty($answer['feedback'])) { unset($node->answers[$key]); } else { - $options[$key] = '
'. check_markup($answer['answer'], $node->filter) .'
'; + // check_markup() over check_plain() because answers use the same input-format as their question! + // read about the difference. + $options[$key] = check_markup($answer['answer'], $node->filter, FALSE); } } - - $form['start'] = array('#type' => 'markup', '#value' => '
'); - $form['question'] = array('#type' => 'markup', '#value' => check_markup($node->body, $node->format)); - +//// or do it here ? +// if ($node->shuffled_answers) { +// shuffle($options); +// } + // Create form $form['tries'] = array( '#type' => $type, - '#options' => $options, + '#prefix' => $prefix, // was form['start'] and form['question'] ... +/// 1st, i dislike that error : +/// "You must select an answer before you can progress to the next question!" +/// what to do if don't know? in a true quiz one can answer other questions +/// before (paper version or when backward is possible) or not answer if one +/// doesn't really know the answer (and remember, we are adding possible +/// negative points on bad choices ; so one shouldn't just pick an answer if +/// not sure)... +/// 2nd, many graphical browsers let the radios/boxes unchecked.. others +/// select the first option exept if there's a default_value (or the last if +/// there are many) : Lynx act so; And HTML specifications are not precise +/// about that... +/// So i suggest we offer a default (or not) first "i dunno" choice :) + '#options' => array_merge(array( -1 => t("i don't know :( ")), $options), '#default_value' => -1, + '#multiple' => $node->multiple_answers, + '#suffix' => $suffix, // would be form['stop'] and maybe form['question'] ... + '#description' => $desc, ); $form['submit'] = array( '#type' => 'submit', @@ -322,12 +500,12 @@ function multichoice_render_question($no * Evaluate whether question is correct * * @param $nid - * Question Node ID + * Question Result Node ID * * @return * Array of results, in the form of: * array( - * 'answers' => array of correct answer(s) + * 'answers' => array of correct answer(s) & feedback(s) + question properties * 'tried' => array of selected answer(s) * ); */ @@ -336,78 +514,92 @@ function multichoice_evaluate_question($ $results = array(); if (isset($_POST['edit']['tries'])) { + $results['answers'] = $question->answers; if (is_array($_POST['edit']['tries'])) { - // Multi-answer question while(list($key, $try) = each($_POST['edit']['tries'])) { - $results['answers'] = $question->answers; $results['tried'][] = $question->answers[$try]['aid']; } } else { - // Single-answer question - $results['answers'] = $question->answers; $results['tried'][] = $question->answers[$_POST['edit']['tries']]['aid']; } + $results['answers']['properties'] = $question->properties; +/// first try was : $results['properties'] = $question->properties; +/// but one call changed too : multichoice_calculate_result($answers, $tried, $properties, $showPoints = FALSE, $showFeedback = FALSE) } //Unset $_POST, otherwise it tries to use the previous answers on the next page... unset($_POST['edit']['tries']); - + //Return the result return $results; } -//Old claculate result function -function multichoice_calculate_result($answers, $tried) { - while(list($key, $answer) = each($answers)) { - if ($answer['points'] == 1) { - if (($key = array_search($answer['aid'], $tried)) !== FALSE) { - //Correct answer was selected, so lets take that out the tried list - unset($tried[$key]); - } - else { - //Correct answer was not in the "tried" list, so score 0 - return 0; - } - } - } - //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; -} - +/** + * Compute a result stats + * + * @param $answers + * Array of correct answer(s) + * @param $tried + * Array of selected answer(s) + * @param $showPoints + * Boolean : says if Points must be shown + * @param $showFeedback + * Boolean : says if Feedback must be shown + * + * @return + * Output array to render, in the form of: + * array( + * 'resultstable' => summary array + * 'winPoints' => current score + * 'maxPoints' => maximum score possible + * 'succed' => whether the quastion is passed (1) or not (0) + * ); + */ //New singing and dancing one -function multichoice_calculate_results($answers, $tried, $showPoints = FALSE, $showFeedback = FALSE) { - //Create results table +function multichoice_calculate_result($answers, $tried, $showPoints = FALSE, $showFeedback = FALSE) { + $winPoints = 0; + $maxPoints = 0; + + //Create results table while computing the score $rows = array(); $correctAnswers = array(); - while(list($key, $answer) = each($answers)) { $cols = array(); - $cols[] = $answer['answer']; - if($showPoints) $cols[] = (($answer['points'] == 0) ? theme_multichoice_unselected() : theme_multichoice_selected()); + if($showPoints) $cols[] = (($answer['points'] <= 0) ? theme_multichoice_unselected() : theme_multichoice_selected()); $isSelected = (array_search($answer['aid'], $tried) !== FALSE); $cols[] = ($isSelected ? theme_multichoice_selected() : theme_multichoice_unselected()); if($showFeedback) $cols[] = ($isSelected ? '
'. $answer['feedback'] .'
' : ''); - + $rows[] = $cols; - - - if($answer['points'] > 0) { - $correctAnswers[] = $answer['aid']; - } + + //Scoring.. begin + if($answer['points'] > 0) { + $correctAnswers[] = $answer['aid']; + if ($answers['properties']['multiple_answers']) { + $maxPoints += $answer['points']; + } + else { + $maxPoints = max($maxPoints, $answer['points']); + } + } + $winPoints += $answer['points']; + + } + + //Scoring.. end + if ($answers['properties']['multiple_answers']) { + //$succed = ($maxPoints == $winPoints) ? 1 : 0 ; + $succed = ($correctAnswers === $tried) ? 1 : 0; + } + else { + $succed = (int)($winPoints > 0); } - - if($correctAnswers === $tried) { $score = 1; } else { $score = 0; } - - return array('score' => $score, 'resultstable' => $rows); + //return array('succed' => $succed, 'resultstable' => $rows, 'maxPoints' => $maxPoints, 'winPoints' => $winPoints); //future way + return $succed; //old way } /** @@ -444,10 +636,11 @@ function theme_multichoice_form($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')), + array('data' => (($node->extend_scoring) ? t('Score') : t('Correct')), ), + array('data' => t('Answer'), 'style' => 'width:45%;', ), +/// sorry 205px breaks my layout + array('data' => t('Feedback'), 'style' => 'width:45%;', ), + array('data' => t('Delete'), ), ); // Format table rows