--- cvs/multichoice.module 2006-11-01 01:04:49.000000000 +0100 +++ local/multichoice.module 2006-11-01 01:30:22.000000000 +0100 @@ -36,7 +36,8 @@ function multichoice_access($op, $node) * Implementation of hook_node_info(). */ function multichoice_node_info() { - return array('multichoice' => array('name' => t('multichoice'), 'base' => 'multichoice')); +/// i think quiz parts (multichoice, matching, and to come) should be prefixed with "quiz - " : so we remember that those node type are not independant and must finaly be part of a quiz. in another hand, doing so let them grouped, what ease things when you want to add a content ( i have not far from 12 content type..!) + return array('multichoice' => array('name' => t('quiz - Multiple choice'), 'base' => 'multichoice')); } /** @@ -46,7 +47,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 +76,12 @@ 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, +/// Much people should be happy with the default settings, so no need to show this.. + '#collapsed' => TRUE, ); // Determine number of answer rows to display @@ -87,6 +91,50 @@ 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' => ($node->multiple_answers ? $node->multiple_answers : 0), + '#options' => $multiple_max_list, + '#description' => t('How many answer(s) can be given for that question ?'), + ); + +/// this is to 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 you click the position you know to be the right, but don't know why)... sure one can simply memorise the full answer, but it may requiere more memory ability especialy when answers look similar... + $form['properties']['shuffled_answers'] = array( + '#type' => 'checkbox', + '#title' => t('shuffle answers order'), + '#default_value' => ($node->shuffled_answers ? $node->shuffled_answers : 0), + '#description' => t('Check that box if the answers shouldn\'t be displayed in the same order'), + ); + +/// this stuff (use of drop-down lists) when answers are short (often one word) and we want them to be displayed inline... example +/// question: choose the right tense for this sentense +/// when he !l! in, we were still playing +/// answers: 1) came +/// 2) comes +/// a selection list is shown here at the location marked !l! ;) + $form['properties']['printable_answers'] = array( + '#type' => 'select', + '#title' => t('display'), + '#default_value' => ($node->printable_answers ? $node->printable_answers : 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 ?'), + ); + $answers = $node->answers; // Display answer rows @@ -96,22 +144,47 @@ 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'], - ); - $form['answers'][$i]['answer'] = array( - '#type' => 'textarea', - '#default_value' => $answers[$i]['answer'], - '#cols' => 30, '#rows' => 2, - ); - $form['answers'][$i]['feedback'] = array( - '#type' => 'textarea', - '#default_value' => $answers[$i]['feedback'], - '#cols' => 30, '#rows' => 2, - ); + +/// thanks to Sean for the idea: 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_rich_scoring', 0), +/// ''#description' => t('Check this if you want to set your own score (from -3 to 5) for each answer.', +/// ); + if(variable_get('quiz_rich_scoring', 0)) { + $form['answers'][$i]['correct'] = array( +/// each answer can have different point(s), not only 1. read clairem's example on content-management-systems (http://drupal.org/comment/reply/15934/26603) + '#type' => 'select', + '#default_value' => $answers[$i]['points'], + '#options' => $points_list, +/// label "Points" ou "Score" should be displayed here because i'm planning to have some others settings in that column.. + '#title' => t('score'), +/// explain/remember the use of that notation. + '#description' => t('>0 if correct.'), + ); + $form['answers'][$i]['answer'] = array( + '#type' => 'textarea', + '#default_value' => $answers[$i]['answer'], + '#cols' => 30, '#rows' => 2, + ); + $form['answers'][$i]['feedback'] = array( + '#type' => 'textarea', + '#default_value' => $answers[$i]['feedback'], + '#cols' => 30, '#rows' => 2, + ); + } + else { + $form['answers'][$i]['correct'] = array( + '#type' => 'select', + '#default_value' => $answers[$i]['points'], + } if ($answers[$i]['aid']) { $form['answers'][$i]['delete'] = array( '#type' => 'checkbox', @@ -128,6 +201,8 @@ function multichoice_form(&$node) { $form['more'] = array( '#type' => 'checkbox', '#title' => t('I need more answers'), +/// 1st part is from poll.module :) 2nd part is something i noticed and i think it must be mentionned even if it may be obvious + '#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; @@ -141,6 +216,9 @@ function multichoice_validate(&$node) { // Hard-code questions to have no teaser and to not be promoted to front page $node->teaser = 0; $node->promote = 0; +/// + $node->sticky = 0; +/// Well. But when testing it's like that it's not effective : if "promote" is checked, the node is promoted... if (!$node->nid && empty($_POST)) return; @@ -155,10 +233,8 @@ function multichoice_validate(&$node) { 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) { +/// we no longer count how non-zero points are returned since each answer can lead to a different score.. and a single choice then can have many correct answers (but some answers are more elegible than other) : see example on cms.. $corrects++; } if ($answer['answer']) { @@ -172,8 +248,17 @@ function multichoice_validate(&$node) { 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))); } } @@ -181,8 +266,8 @@ function multichoice_validate(&$node) { * 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))); + 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']); @@ -193,13 +278,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)), $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 @@ -225,19 +311,20 @@ 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']; + return $additions; } @@ -281,17 +368,72 @@ function multichoice_help($section) { * HTML output */ function multichoice_render_question($node) { - // Radio buttons for single selection questions, checkboxes for multiselect + $question = check_markup($node->body, $node->format); + $ok = eregi("![a-z]!", $question, $tag); + $prefix = '
'; // was class="multichoice_form" + $prefix .= '
'; // +/// it's another boolean, so the test must be if ($node->printable_answers) But i test ($node->printable_answers != 0) in order to preserve compatibility.. in fact, when first use after the patch is applied, {quiz_question}.properties column is not update according to the new structure introduced.. + if ($node->printable_answers != 0) { + /// full interface : should always be the default. + + // Radio buttons for single selection questions, checkboxes for multiselect + if (!$node->multiple_answers) { + $type = 'radios'; + } + else { + $type = 'checkboxes'; + } + + if ($ok) { + // remove useless the 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" + + } + else { + /// compact interface : here having a default answer ("i've no answer") is a good thing. but, this kind of interface is a very bad idea because the answer/choice may not be one-line... + + // 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, 0, strpos($question, $tag[0], 0) + 3); + } + $suffix = '
'; // class="quiz_question" + + } + + $suffix .= ''; // class="quiz_form" if ($node->multiple_answers == 0) { - $type = 'radios'; + $desc = t('Please choose the right aswer'); } else { - $type = 'checkboxes'; + //$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))); } + // Get options $options = array(); - + if ($node->shuffled_answers != 0) { + shuffle($node->answers); + } while(list($key, $answer) = each($node->answers)) { if (empty($answer['correct']) && empty($answer['answer']) && empty($answer['feedback'])) { unset($node->answers[$key]); @@ -300,14 +442,24 @@ function multichoice_render_question($no $options[$key] = $answer['answer']; } } - - $form['question'] = array('#type' => 'markup', '#value' => $node->body); - +/// if ($node->shuffled_answers != 0) { +/// shuffle($options); +/// } + +/// 1st, i dislike that error : "You must select an answer before you can progress to the next question!" what to do i don't know ? in a true quiz one can answer other questions before (paper version or backward possible) or not answer if one doesn't really know the answer (and remember, we are adding possible negative points for bad choices ; so one one shouldn't just pick an answer if not sure)... +/// 2nd, many graphical browsers let the radios/boxes unchecked.. others select the first (or the last) option until precise default_value is encounted : 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); + // Create form $form['tries'] = array( + '#prefix' => $prefix, '#type' => $type, '#options' => $options, '#default_value' => -1, + '#multiple' => $node->multiple_answers, + '#suffix' => $suffix, + '#description' => $desc, ); $form['submit'] = array( '#type' => 'submit', @@ -321,12 +473,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) * ); */ @@ -335,78 +487,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; } //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 = TRUE, $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()); +/// wrong answer is no longer "== 0" but "<= 0" + 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']; - } + +/// correct answer is no longer "== 1" but "> 0" + //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 } /** @@ -443,10 +609,12 @@ 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')), +/// we no longer just say it's correct (1 point) or not (0 point) so... (for now i prefer points rather than score because it's not use in the score computation (score computation show % of correct answer, not the number of points gained...) + array('data' => t('Points')), + array('data' => t('Answer'), 'style' => 'width:42%;'), +/// sorry 205px breaks my layout + array('data' => t('Feedback'), 'style' => 'width:42%;'), + array('data' => t('Delete'), 'style' => 'width:2%;'), ); // Format table rows