diff --git a/question/type/ordering/lang/en/qtype_ordering.php b/question/type/ordering/lang/en/qtype_ordering.php index 3d5087ab97c..f120493fb6d 100644 --- a/question/type/ordering/lang/en/qtype_ordering.php +++ b/question/type/ordering/lang/en/qtype_ordering.php @@ -1,4 +1,4 @@ -id; + } + + public function format_questiontext($qa) { + $text = parent::format_questiontext($qa); + return stripslashes($text); + } public function get_expected_data() { - return array('answer' => PARAM_INTEGER); + $name = $this->get_response_fieldname(); + return array($name => PARAM_TEXT); } public function get_correct_response() { - return array('answer' => (int) $this->rightanswer); + if ($this->rightanswer===null) { + $this->rightanswer = $this->get_ordering_answers(); + $this->rightanswer = array_keys($this->rightanswer); + $this->rightanswer = implode(',', $this->rightanswer); + } + return array('answer' => $this->rightanswer); } public function summarise_response(array $response) { @@ -59,10 +73,56 @@ class qtype_ordering_question extends question_graded_automatically { } } + public function get_ordering_options() { + global $DB; + if ($this->options===null) { + $this->options = $DB->get_record('question_ordering', array('question' => $this->id)); + if ($this->options==false) { + $this->options = (object)array( + 'question' => $this->id, + 'logical' => 0, // require all answers + 'studentsee' => 0, + 'correctfeedback' => '', + 'incorrectfeedback' => '', + 'partiallycorrectfeedback' => '' + ); + $this->options->id = $DB->insert_record('question_ordering', $options); + } + } + return $this->options; + } + + public function get_ordering_answers() { + global $CFG, $DB; + if ($this->answers===null) { + $this->answers = $DB->get_records('question_answers', array('question' => $this->id), 'fraction,id'); + if ($this->answers) { + if (isset($CFG->passwordsaltmain)) { + $salt = $CFG->passwordsaltmain; + } else { + $salt = ''; // complex_random_string() + } + foreach ($this->answers as $answerid => $answer) { + $this->answers[$answerid]->md5key = 'ordering_item_'.md5($salt.$answer->answer); + } + } else { + $this->answers = array(); + } + } + return $this->answers; + } + public function is_complete_response(array $response) { global $CFG, $DB; - $responses = explode(',', $_POST['q'.$this->id]); + $name = $this->get_response_fieldname(); + if (isset($response[$name])) { + $responses = $response[$name]; + } else { + $responses = optional_param($name, '', PARAM_TEXT); + } + $responses = preg_replace('[^a-zA-Z0-9,_-]', '', $responses); + $responses = explode(',', $responses); // convert to array $responses = array_filter($responses); // remove blanks $responses = array_unique($responses); // remove duplicates @@ -74,23 +134,25 @@ class qtype_ordering_question extends question_graded_automatically { } } - if (! $options = $DB->get_record ('question_ordering', array('question' => $this->id))) { - $options = (object)array('logical' => 0); // shouldn't happen !! - } - if (! $answers = $DB->get_records ('question_answers', array('question' => $this->id), 'fraction,id')) { - $answers = array(); // shouldn't happen !! - } + $ordering = $this->get_ordering_options(); + $answers = $this->get_ordering_answers(); - if ($options->logical==0) { + if ($ordering->logical==0) { $total = count($answers); // require all answers } else { - $total = $options->studentsee + 2; // a subset of answers + $total = $ordering->studentsee + 2; // a subset of answers + } + + if (isset($CFG->passwordsaltmain)) { + $salt = $CFG->passwordsaltmain; + } else { + $salt = ''; // complex_random_string() } $validresponses = array(); foreach ($answers as $answerid => $answer) { - $response = md5($CFG->passwordsaltmain.$answer->answer); + $response = md5($salt.$answer->answer); $sortorder = intval($answer->fraction); if (in_array($response, $responses)) { @@ -165,7 +227,7 @@ class qtype_ordering_question extends question_graded_automatically { return array($fraction, question_state::graded_state_for_fraction($fraction)); } - public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { - + public function check_file_access($qa, $ordering, $component, $filearea, $args, $forcedownload) { + // do nothing } } diff --git a/question/type/ordering/questiontype.php b/question/type/ordering/questiontype.php index c3464aa5f6e..5bc3b2729f7 100644 --- a/question/type/ordering/questiontype.php +++ b/question/type/ordering/questiontype.php @@ -41,9 +41,10 @@ if (class_exists('question_type')) { */ class qtype_ordering extends question_type { - public function is_not_blank($value) { - $value = trim($value); - return ($value || $value==='0'); + protected function initialise_question_instance(question_definition $question, $questiondata) { + parent::initialise_question_instance($question, $questiondata); + $answers = array_keys($questiondata->options->answers); + $question->rightanswer = implode(',', $answers); } public function save_question_options($question) { @@ -128,6 +129,11 @@ class qtype_ordering extends question_type { return true; } + public function is_not_blank($value) { + $value = trim($value); + return ($value || $value==='0'); + } + public function get_question_options($question) { global $DB, $OUTPUT; diff --git a/question/type/ordering/renderer.php b/question/type/ordering/renderer.php index a36566c7eef..7fbe7cb0b76 100644 --- a/question/type/ordering/renderer.php +++ b/question/type/ordering/renderer.php @@ -35,53 +35,69 @@ class qtype_ordering_renderer extends qtype_renderer { public function formulation_and_controls(question_attempt $qa, question_display_options $options) { global $CFG, $DB; + static $addStyle = true; static $addScript = true; $question = $qa->get_question(); + $ordering = $question->get_ordering_options(); + $answers = $question->get_ordering_answers(); - if (! $options = $DB->get_record('question_ordering', array('question' => $question->id))) { - return ''; - } - - if (! $answers = $DB->get_records('question_answers', array('question' => $question->id), '', '*')) { + if (empty($ordering) || empty($answers)) { return ''; // shouldn't happen !! } - if ($options->studentsee==0) { // all items - $options->studentsee = count($answers); + if ($ordering->studentsee==0) { // all items + $ordering->studentsee = count($answers); } else { // a nasty hack so that "studentsee" is the same // as what is displayed by edit_ordering_form.php - $options->studentsee += 2; + $ordering->studentsee += 2; } - switch ($options->logical) { - - case 0: // all - $answerids = array_keys($answers); - break; - - case 1: // random subset - $answerids = array_rand($answers, $options->studentsee); - break; - - case 2: // contiguous subset - if (count($answers) > $options->studentsee) { - $offset = mt_rand(0, count($answers) - $options->studentsee); - $answers = array_slice($answers, $offset, $options->studentsee, true); - } - $answerids = array_keys($answers); - break; + if ($options->readonly || $options->correctness) { + // don't allow items to be dragged and dropped + $readonly = true; + } else { + $readonly = false; } - shuffle($answerids); + + if ($options->correctness) { + list($answerids, $correctorder) = $this->get_response($qa, $question, $answers); + } else { + $correctorder = array(); + switch ($ordering->logical) { + case 0: // all + $answerids = array_keys($answers); + break; + + case 1: // random subset + $answerids = array_rand($answers, $ordering->studentsee); + break; + + case 2: // contiguous subset + if (count($answers) > $ordering->studentsee) { + $offset = mt_rand(0, count($answers) - $ordering->studentsee); + $answers = array_slice($answers, $offset, $ordering->studentsee, true); + } + $answerids = array_keys($answers); + break; + } + shuffle($answerids); + } + + $response_name = $qa->get_qt_field_name($question->get_response_fieldname()); + $response_id = 'id_'.preg_replace('/[^a-zA-Z0-9]+/', '_', $response_name); + $sortable_id = 'id_sortable_'.$question->id; $result = ''; - $result .= html_writer::tag('script', '', array('type'=>'text/javascript', 'src'=>$CFG->wwwroot.'/question/type/ordering/js/jquery.js')); - $result .= html_writer::tag('script', '', array('type'=>'text/javascript', 'src'=>$CFG->wwwroot.'/question/type/ordering/js/jquery-ui.js')); + if ($readonly==false) { + $result .= html_writer::tag('script', '', array('type'=>'text/javascript', 'src'=>$CFG->wwwroot.'/question/type/ordering/js/jquery.js')); + $result .= html_writer::tag('script', '', array('type'=>'text/javascript', 'src'=>$CFG->wwwroot.'/question/type/ordering/js/jquery-ui.js')); + } $style = "\n"; - $style .= "ul.sortable".$question->id." li {\n"; + $style .= "ul#$sortable_id li {\n"; $style .= " position: relative;\n"; $style .= "}\n"; if ($addStyle) { @@ -101,7 +117,9 @@ class qtype_ordering_renderer extends qtype_renderer { $style .= " background-color: #eeeeee;\n"; $style .= " border: 1px solid #cccccc;\n"; $style .= " border-image: initial;\n"; - $style .= " cursor: move;\n"; + if ($readonly==false) { + $style .= " cursor: move;\n"; + } $style .= " list-style-type: none;\n"; $style .= " margin-bottom: 1px;\n"; $style .= " min-height: 20px;\n"; @@ -110,92 +128,109 @@ class qtype_ordering_renderer extends qtype_renderer { } $result .= html_writer::tag('style', $style, array('type' => 'text/css')); - $script = "\n"; - $script .= "//id."').sortable({\n"; - $script .= " update: function(event, ui) {\n"; - $script .= " var ItemsOrder = $(this).sortable('toArray').toString();\n"; - $script .= " $('#q".$question->id."').attr('value', ItemsOrder);\n"; - $script .= " }\n"; - $script .= " });\n"; - $script .= " $('#sortable".$question->id."').disableSelection();\n"; - $script .= "});\n"; - $script .= "$(document).ready(function() {\n"; - $script .= " var ItemsOrder = $('#sortable".$question->id."').sortable('toArray').toString();\n"; - $script .= " $('#q".$question->id."').attr('value', ItemsOrder);\n"; - $script .= "});\n"; - $script .= "//]]>\n"; - $result .= html_writer::tag('script', $script, array('type' => 'text/javascript')); - - $result .= html_writer::tag('div', stripslashes($question->format_questiontext($qa)), array('class' => 'qtext')); - $result .= html_writer::start_tag('div', array('class' => 'ablock')); - $result .= html_writer::start_tag('div', array('class' => 'answer')); - $result .= html_writer::start_tag('ul', array('class' => 'boxy', 'id' => 'sortable'.$question->id)); - - // a salt (=random string) to help disguise answer ids - if (isset($CFG->passwordsaltmain)) { - $salt = $CFG->passwordsaltmain; - } else { - $salt = complex_random_string(); - } - - // generate ordering items - foreach ($answerids as $i => $answerid) { - // the original "id" revealed the correct order of the answers - // because $answer->fraction holds the correct order number - // $id = 'ordering_item_'.$answerid.'_'.intval($answers[$answerid]->fraction); - $id = 'ordering_item_'.md5($salt.$answers[$answerid]->answer); - $params = array('class' => 'ui-state-default', 'id' => $id); - $result .= html_writer::tag('li', $answers[$answerid]->answer, $params); - } - - $result .= html_writer::end_tag('ul'); - $result .= html_writer::end_tag('div'); // answer - $result .= html_writer::end_tag('div'); // ablock - - $result .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'q'.$question->id, 'id' => 'q'.$question->id, 'value' => '9')); - $result .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'answer', 'value' => '')); - - $result .= html_writer::tag('div', '', array('style' => 'clear:both;')); - - $script = "\n"; - $script .= "//\n"; + $result .= html_writer::tag('script', $script, array('type' => 'text/javascript')); + } + + $result .= html_writer::tag('div', $question->format_questiontext($qa), array('class' => 'qtext')); + + if (count($answerids)) { + $result .= html_writer::start_tag('div', array('class' => 'ablock')); + $result .= html_writer::start_tag('div', array('class' => 'answer')); + $result .= html_writer::start_tag('ul', array('class' => 'boxy', 'id' => $sortable_id)); + + // generate ordering items + foreach ($answerids as $position => $answerid) { + if (array_key_exists($answerid, $answers)) { + if ($options->correctness) { + if ($correctorder[$position]==$answerid) { + $class = 'correctposition'; + $img = $this->feedback_image(1); + } else { + $class = 'wrongposition'; + $img = $this->feedback_image(0); + } + $img = "$img "; + } else { + $class = 'ui-state-default'; + $img = ''; + } + // the original "id" revealed the correct order of the answers + // because $answer->fraction holds the correct order number + // $id = 'ordering_item_'.$answerid.'_'.intval($answers[$answerid]->fraction); + $params = array('class' => $class, 'id' => $answers[$answerid]->md5key); + + $result .= html_writer::tag('li', $img.$answers[$answerid]->answer, $params); + } + } + + $result .= html_writer::end_tag('ul'); + $result .= html_writer::end_tag('div'); // answer + $result .= html_writer::end_tag('div'); // ablock + + $params = array('type' => 'hidden', 'name' => $response_name, 'id' => $response_id, 'value' => ''); + $result .= html_writer::empty_tag('input', $params); + $result .= html_writer::tag('div', '', array('style' => 'clear:both;')); + } + + if ($readonly==false) { + $script = "\n"; + $script .= "//\n"; + $result .= html_writer::tag('script', $script, array('type' => 'text/javascript')); } - $script .= "orderingInit('sortable".$question->id."');\n"; - $script .= "//]]>\n"; - $result .= html_writer::tag('script', $script, array('type' => 'text/javascript')); return $result; } @@ -203,21 +238,75 @@ class qtype_ordering_renderer extends qtype_renderer { public function correct_response(question_attempt $qa) { global $DB; - $question = $qa->get_question(); + $output = ''; - if (! $step = $DB->get_records('question_attempt_steps', array('questionattemptid' => $question->contextid), 'id DESC')) { - return ''; // shouldn't happen !! - } - $step = current($step); // first one - - if ($step->fraction >= 1) { - $feedback = get_string('correctfeedback', 'qtype_ordering'); - } else if ($step->fraction > 0) { - $feedback = get_string('partiallycorrectfeedback', 'qtype_ordering').' '.round($step->fraction, 2); - } else { - $feedback = get_string('incorrectfeedback', 'qtype_ordering'); + $showcorrect = false; + if ($step = $qa->get_last_step()) { + switch ($step->get_state()) { + case 'gradedright': + $msg = get_string('correctfeedback', 'qtype_ordering'); + break; + case 'gradedpartial': + $showcorrect = true; + $fraction = round($step->get_fraction(), 2); + $msg = get_string('partiallycorrectfeedback', 'qtype_ordering', $fraction); + break; + case 'gradedwrong': + $showcorrect = true; + $msg = get_string('incorrectfeedback', 'qtype_ordering'); + break; + default: + $msg = ''; + } + if ($msg) { + $output .= html_writer::tag('p', $msg); + } } - return $feedback; + if ($showcorrect) { + $question = $qa->get_question(); + $answers = $question->get_ordering_answers(); + list($answerids, $correctorder) = $this->get_response($qa, $question, $answers); + if (count($correctorder)) { + $output .= html_writer::tag('p', get_string('correctorder', 'qtype_ordering')); + $output .= html_writer::start_tag('ol'); + foreach ($correctorder as $position => $answerid) { + $output .= html_writer::tag('li', $answers[$answerid]->answer); + } + $output .= html_writer::end_tag('ol'); + } else { + $output .= html_writer::tag('p', get_string('noresponsedetails', 'qtype_ordering')); + } + } + + return $output; + } + + public function get_response($qa, $question, $answers) { + $answerids = array(); + $correctorder = array(); + + $name = $question->get_response_fieldname(); + if ($step = $qa->get_last_step_with_qt_var($name)) { + + $response = $step->get_qt_var($name); // "$md5key, ..." + $response = explode(',', $response); // array($position => $md5key, ...) + $response = array_flip($response); // array($md5key => $position, ...) + + foreach ($answers as $answer) { + if (array_key_exists($answer->md5key, $response)) { + $position = $response[$answer->md5key]; + $sortorder = intval($answer->fraction); + $answerids[$position] = $answer->id; + $correctorder[$sortorder] = $answer->id; + } + } + + ksort($answerids); + ksort($correctorder); + $correctorder = array_values($correctorder); + } + + return array($answerids, $correctorder); } } diff --git a/question/type/ordering/styles.css b/question/type/ordering/styles.css index e69de29bb2d..e8c86cbfa08 100644 --- a/question/type/ordering/styles.css +++ b/question/type/ordering/styles.css @@ -0,0 +1,11 @@ +ul.boxy li.correctposition { + border: 4px solid #99ff66; /* gentle green */ +} +ul.boxy li.wrongposition { + border: 4px solid #ff7373; /* gentle red */ +} +ul.boxy li.correctposition, +ul.boxy li.wrongposition { + margin-bottom: 4px; + margin-top: 4px; +} diff --git a/question/type/ordering/version.php b/question/type/ordering/version.php index 79dbb0d103d..42145f769c7 100644 --- a/question/type/ordering/version.php +++ b/question/type/ordering/version.php @@ -30,6 +30,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->cron = 0; $plugin->component = 'qtype_ordering'; $plugin->maturity = MATURITY_STABLE; // ALPHA=50, BETA=100, RC=150, STABLE=200 -$plugin->release = '2014-05-14.01'; -$plugin->version = 2014051401; +$plugin->release = '2014-07-26 (02)'; +$plugin->version = 2014072602; $plugin->requires = 2010112400; // Moodle 2.0