Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Renderers for outputting parts of the question engine. 19 * 20 * @package moodlecore 21 * @subpackage questionengine 22 * @copyright 2009 The Open University 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 30 /** 31 * This renderer controls the overall output of questions. It works with a 32 * {@link qbehaviour_renderer} and a {@link qtype_renderer} to output the 33 * type-specific bits. The main entry point is the {@link question()} method. 34 * 35 * @copyright 2009 The Open University 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class core_question_renderer extends plugin_renderer_base { 39 public function get_page() { 40 return $this->page; 41 } 42 43 /** 44 * Render an icon, optionally with the word 'Preview' beside it, to preview 45 * a given question. 46 * @param int $questionid the id of the question to be previewed. 47 * @param context $context the context in which the preview is happening. 48 * Must be a course or category context. 49 * @param bool $showlabel if true, show the word 'Preview' after the icon. 50 * If false, just show the icon. 51 */ 52 public function question_preview_link($questionid, context $context, $showlabel) { 53 if ($showlabel) { 54 $alt = ''; 55 $label = get_string('preview'); 56 $attributes = array(); 57 } else { 58 $alt = get_string('preview'); 59 $label = ''; 60 $attributes = array('title' => $alt); 61 } 62 63 $image = $this->pix_icon('t/preview', $alt, '', array('class' => 'iconsmall')); 64 $link = question_preview_url($questionid, null, null, null, null, $context); 65 $action = new popup_action('click', $link, 'questionpreview', 66 question_preview_popup_params()); 67 68 return $this->action_link($link, $image . $label, $action, $attributes); 69 } 70 71 /** 72 * Generate the display of a question in a particular state, and with certain 73 * display options. Normally you do not call this method directly. Intsead 74 * you call {@link question_usage_by_activity::render_question()} which will 75 * call this method with appropriate arguments. 76 * 77 * @param question_attempt $qa the question attempt to display. 78 * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour 79 * specific parts. 80 * @param qtype_renderer $qtoutput the renderer to output the question type 81 * specific parts. 82 * @param question_display_options $options controls what should and should not be displayed. 83 * @param string|null $number The question number to display. 'i' is a special 84 * value that gets displayed as Information. Null means no number is displayed. 85 * @return string HTML representation of the question. 86 */ 87 public function question(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 88 qtype_renderer $qtoutput, question_display_options $options, $number) { 89 90 $output = ''; 91 $output .= html_writer::start_tag('div', array( 92 'id' => $qa->get_outer_question_div_unique_id(), 93 'class' => implode(' ', array( 94 'que', 95 $qa->get_question(false)->get_type_name(), 96 $qa->get_behaviour_name(), 97 $qa->get_state_class($options->correctness && $qa->has_marks()), 98 )) 99 )); 100 101 $output .= html_writer::tag('div', 102 $this->info($qa, $behaviouroutput, $qtoutput, $options, $number), 103 array('class' => 'info')); 104 105 $output .= html_writer::start_tag('div', array('class' => 'content')); 106 107 $output .= html_writer::tag('div', 108 $this->add_part_heading($qtoutput->formulation_heading(), 109 $this->formulation($qa, $behaviouroutput, $qtoutput, $options)), 110 array('class' => 'formulation clearfix')); 111 $output .= html_writer::nonempty_tag('div', 112 $this->add_part_heading(get_string('feedback', 'question'), 113 $this->outcome($qa, $behaviouroutput, $qtoutput, $options)), 114 array('class' => 'outcome clearfix')); 115 $output .= html_writer::nonempty_tag('div', 116 $this->add_part_heading(get_string('comments', 'question'), 117 $this->manual_comment($qa, $behaviouroutput, $qtoutput, $options)), 118 array('class' => 'comment clearfix')); 119 $output .= html_writer::nonempty_tag('div', 120 $this->response_history($qa, $behaviouroutput, $qtoutput, $options), 121 array('class' => 'history clearfix border p-2')); 122 123 $output .= html_writer::end_tag('div'); 124 $output .= html_writer::end_tag('div'); 125 return $output; 126 } 127 128 /** 129 * Generate the information bit of the question display that contains the 130 * metadata like the question number, current state, and mark. 131 * @param question_attempt $qa the question attempt to display. 132 * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour 133 * specific parts. 134 * @param qtype_renderer $qtoutput the renderer to output the question type 135 * specific parts. 136 * @param question_display_options $options controls what should and should not be displayed. 137 * @param string|null $number The question number to display. 'i' is a special 138 * value that gets displayed as Information. Null means no number is displayed. 139 * @return HTML fragment. 140 */ 141 protected function info(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 142 qtype_renderer $qtoutput, question_display_options $options, $number) { 143 $output = ''; 144 $output .= $this->number($number); 145 $output .= $this->status($qa, $behaviouroutput, $options); 146 $output .= $this->mark_summary($qa, $behaviouroutput, $options); 147 $output .= $this->question_flag($qa, $options->flags); 148 $output .= $this->edit_question_link($qa, $options); 149 return $output; 150 } 151 152 /** 153 * Generate the display of the question number. 154 * @param string|null $number The question number to display. 'i' is a special 155 * value that gets displayed as Information. Null means no number is displayed. 156 * @return HTML fragment. 157 */ 158 protected function number($number) { 159 if (trim($number) === '') { 160 return ''; 161 } 162 $numbertext = ''; 163 if (trim($number) === 'i') { 164 $numbertext = get_string('information', 'question'); 165 } else { 166 $numbertext = get_string('questionx', 'question', 167 html_writer::tag('span', $number, array('class' => 'qno'))); 168 } 169 return html_writer::tag('h3', $numbertext, array('class' => 'no')); 170 } 171 172 /** 173 * Add an invisible heading like 'question text', 'feebdack' at the top of 174 * a section's contents, but only if the section has some content. 175 * @param string $heading the heading to add. 176 * @param string $content the content of the section. 177 * @return string HTML fragment with the heading added. 178 */ 179 protected function add_part_heading($heading, $content) { 180 if ($content) { 181 $content = html_writer::tag('h4', $heading, array('class' => 'accesshide')) . $content; 182 } 183 return $content; 184 } 185 186 /** 187 * Generate the display of the status line that gives the current state of 188 * the question. 189 * @param question_attempt $qa the question attempt to display. 190 * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour 191 * specific parts. 192 * @param question_display_options $options controls what should and should not be displayed. 193 * @return HTML fragment. 194 */ 195 protected function status(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 196 question_display_options $options) { 197 return html_writer::tag('div', $qa->get_state_string($options->correctness), 198 array('class' => 'state')); 199 } 200 201 /** 202 * Generate the display of the marks for this question. 203 * @param question_attempt $qa the question attempt to display. 204 * @param qbehaviour_renderer $behaviouroutput the behaviour renderer, which can generate a custom display. 205 * @param question_display_options $options controls what should and should not be displayed. 206 * @return HTML fragment. 207 */ 208 protected function mark_summary(question_attempt $qa, qbehaviour_renderer $behaviouroutput, question_display_options $options) { 209 return html_writer::nonempty_tag('div', 210 $behaviouroutput->mark_summary($qa, $this, $options), 211 array('class' => 'grade')); 212 } 213 214 /** 215 * Generate the display of the marks for this question. 216 * @param question_attempt $qa the question attempt to display. 217 * @param question_display_options $options controls what should and should not be displayed. 218 * @return HTML fragment. 219 */ 220 public function standard_mark_summary(question_attempt $qa, qbehaviour_renderer $behaviouroutput, question_display_options $options) { 221 if (!$options->marks) { 222 return ''; 223 224 } else if ($qa->get_max_mark() == 0) { 225 return get_string('notgraded', 'question'); 226 227 } else if ($options->marks == question_display_options::MAX_ONLY || 228 is_null($qa->get_fraction())) { 229 return $behaviouroutput->marked_out_of_max($qa, $this, $options); 230 231 } else { 232 return $behaviouroutput->mark_out_of_max($qa, $this, $options); 233 } 234 } 235 236 /** 237 * Generate the display of the available marks for this question. 238 * @param question_attempt $qa the question attempt to display. 239 * @param question_display_options $options controls what should and should not be displayed. 240 * @return HTML fragment. 241 */ 242 public function standard_marked_out_of_max(question_attempt $qa, question_display_options $options) { 243 return get_string('markedoutofmax', 'question', $qa->format_max_mark($options->markdp)); 244 } 245 246 /** 247 * Generate the display of the marks for this question out of the available marks. 248 * @param question_attempt $qa the question attempt to display. 249 * @param question_display_options $options controls what should and should not be displayed. 250 * @return HTML fragment. 251 */ 252 public function standard_mark_out_of_max(question_attempt $qa, question_display_options $options) { 253 $a = new stdClass(); 254 $a->mark = $qa->format_mark($options->markdp); 255 $a->max = $qa->format_max_mark($options->markdp); 256 return get_string('markoutofmax', 'question', $a); 257 } 258 259 /** 260 * Render the question flag, assuming $flagsoption allows it. 261 * 262 * @param question_attempt $qa the question attempt to display. 263 * @param int $flagsoption the option that says whether flags should be displayed. 264 */ 265 protected function question_flag(question_attempt $qa, $flagsoption) { 266 global $CFG; 267 268 $divattributes = array('class' => 'questionflag'); 269 270 switch ($flagsoption) { 271 case question_display_options::VISIBLE: 272 $flagcontent = $this->get_flag_html($qa->is_flagged()); 273 break; 274 275 case question_display_options::EDITABLE: 276 $id = $qa->get_flag_field_name(); 277 // The checkbox id must be different from any element name, because 278 // of a stupid IE bug: 279 // http://www.456bereastreet.com/archive/200802/beware_of_id_and_name_attribute_mixups_when_using_getelementbyid_in_internet_explorer/ 280 $checkboxattributes = array( 281 'type' => 'checkbox', 282 'id' => $id . 'checkbox', 283 'name' => $id, 284 'value' => 1, 285 ); 286 if ($qa->is_flagged()) { 287 $checkboxattributes['checked'] = 'checked'; 288 } 289 $postdata = question_flags::get_postdata($qa); 290 291 $flagcontent = html_writer::empty_tag('input', 292 array('type' => 'hidden', 'name' => $id, 'value' => 0)) . 293 html_writer::empty_tag('input', $checkboxattributes) . 294 html_writer::empty_tag('input', 295 array('type' => 'hidden', 'value' => $postdata, 'class' => 'questionflagpostdata')) . 296 html_writer::tag('label', $this->get_flag_html($qa->is_flagged(), $id . 'img'), 297 array('id' => $id . 'label', 'for' => $id . 'checkbox')) . "\n"; 298 299 $divattributes = array( 300 'class' => 'questionflag editable', 301 'aria-atomic' => 'true', 302 'aria-relevant' => 'text', 303 'aria-live' => 'assertive', 304 ); 305 306 break; 307 308 default: 309 $flagcontent = ''; 310 } 311 312 return html_writer::nonempty_tag('div', $flagcontent, $divattributes); 313 } 314 315 /** 316 * Work out the actual img tag needed for the flag 317 * 318 * @param bool $flagged whether the question is currently flagged. 319 * @param string $id an id to be added as an attribute to the img (optional). 320 * @return string the img tag. 321 */ 322 protected function get_flag_html($flagged, $id = '') { 323 if ($flagged) { 324 $icon = 'i/flagged'; 325 $alt = get_string('flagged', 'question'); 326 $label = get_string('clickunflag', 'question'); 327 } else { 328 $icon = 'i/unflagged'; 329 $alt = get_string('notflagged', 'question'); 330 $label = get_string('clickflag', 'question'); 331 } 332 $attributes = array( 333 'src' => $this->image_url($icon), 334 'alt' => $alt, 335 'class' => 'questionflagimage', 336 ); 337 if ($id) { 338 $attributes['id'] = $id; 339 } 340 $img = html_writer::empty_tag('img', $attributes); 341 $img .= html_writer::span($label); 342 343 return $img; 344 } 345 346 protected function edit_question_link(question_attempt $qa, 347 question_display_options $options) { 348 global $CFG; 349 350 if (empty($options->editquestionparams)) { 351 return ''; 352 } 353 354 $params = $options->editquestionparams; 355 if ($params['returnurl'] instanceof moodle_url) { 356 $params['returnurl'] = $params['returnurl']->out_as_local_url(false); 357 } 358 $params['id'] = $qa->get_question_id(); 359 $editurl = new moodle_url('/question/question.php', $params); 360 361 return html_writer::tag('div', html_writer::link( 362 $editurl, $this->pix_icon('t/edit', get_string('edit'), '', array('class' => 'iconsmall')) . 363 get_string('editquestion', 'question')), 364 array('class' => 'editquestion')); 365 } 366 367 /** 368 * Generate the display of the formulation part of the question. This is the 369 * area that contains the quetsion text, and the controls for students to 370 * input their answers. Some question types also embed feedback, for 371 * example ticks and crosses, in this area. 372 * 373 * @param question_attempt $qa the question attempt to display. 374 * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour 375 * specific parts. 376 * @param qtype_renderer $qtoutput the renderer to output the question type 377 * specific parts. 378 * @param question_display_options $options controls what should and should not be displayed. 379 * @return HTML fragment. 380 */ 381 protected function formulation(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 382 qtype_renderer $qtoutput, question_display_options $options) { 383 $output = ''; 384 $output .= html_writer::empty_tag('input', array( 385 'type' => 'hidden', 386 'name' => $qa->get_control_field_name('sequencecheck'), 387 'value' => $qa->get_sequence_check_count())); 388 $output .= $qtoutput->formulation_and_controls($qa, $options); 389 if ($options->clearwrong) { 390 $output .= $qtoutput->clear_wrong($qa); 391 } 392 $output .= html_writer::nonempty_tag('div', 393 $behaviouroutput->controls($qa, $options), array('class' => 'im-controls')); 394 return $output; 395 } 396 397 /** 398 * Generate the display of the outcome part of the question. This is the 399 * area that contains the various forms of feedback. 400 * 401 * @param question_attempt $qa the question attempt to display. 402 * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour 403 * specific parts. 404 * @param qtype_renderer $qtoutput the renderer to output the question type 405 * specific parts. 406 * @param question_display_options $options controls what should and should not be displayed. 407 * @return HTML fragment. 408 */ 409 protected function outcome(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 410 qtype_renderer $qtoutput, question_display_options $options) { 411 $output = ''; 412 $output .= html_writer::nonempty_tag('div', 413 $qtoutput->feedback($qa, $options), array('class' => 'feedback')); 414 $output .= html_writer::nonempty_tag('div', 415 $behaviouroutput->feedback($qa, $options), array('class' => 'im-feedback')); 416 $output .= html_writer::nonempty_tag('div', 417 $options->extrainfocontent, array('class' => 'extra-feedback')); 418 return $output; 419 } 420 421 protected function manual_comment(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 422 qtype_renderer $qtoutput, question_display_options $options) { 423 return $qtoutput->manual_comment($qa, $options) . 424 $behaviouroutput->manual_comment($qa, $options); 425 } 426 427 /** 428 * Generate the display of the response history part of the question. This 429 * is the table showing all the steps the question has been through. 430 * 431 * @param question_attempt $qa the question attempt to display. 432 * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour 433 * specific parts. 434 * @param qtype_renderer $qtoutput the renderer to output the question type 435 * specific parts. 436 * @param question_display_options $options controls what should and should not be displayed. 437 * @return HTML fragment. 438 */ 439 protected function response_history(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 440 qtype_renderer $qtoutput, question_display_options $options) { 441 442 if (!$options->history) { 443 return ''; 444 } 445 446 $table = new html_table(); 447 $table->head = array ( 448 get_string('step', 'question'), 449 get_string('time'), 450 get_string('action', 'question'), 451 get_string('state', 'question'), 452 ); 453 if ($options->marks >= question_display_options::MARK_AND_MAX) { 454 $table->head[] = get_string('marks', 'question'); 455 } 456 457 foreach ($qa->get_full_step_iterator() as $i => $step) { 458 $stepno = $i + 1; 459 460 $rowclass = ''; 461 if ($stepno == $qa->get_num_steps()) { 462 $rowclass = 'current'; 463 } else if (!empty($options->questionreviewlink)) { 464 $url = new moodle_url($options->questionreviewlink, 465 array('slot' => $qa->get_slot(), 'step' => $i)); 466 $stepno = $this->output->action_link($url, $stepno, 467 new popup_action('click', $url, 'reviewquestion', 468 array('width' => 450, 'height' => 650)), 469 array('title' => get_string('reviewresponse', 'question'))); 470 } 471 472 $restrictedqa = new question_attempt_with_restricted_history($qa, $i, null); 473 474 $user = new stdClass(); 475 $user->id = $step->get_user_id(); 476 $row = array( 477 $stepno, 478 userdate($step->get_timecreated(), get_string('strftimedatetimeshort')), 479 s($qa->summarise_action($step)), 480 $restrictedqa->get_state_string($options->correctness), 481 ); 482 483 if ($options->marks >= question_display_options::MARK_AND_MAX) { 484 $row[] = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp); 485 } 486 487 $table->rowclasses[] = $rowclass; 488 $table->data[] = $row; 489 } 490 491 return html_writer::tag('h4', get_string('responsehistory', 'question'), 492 array('class' => 'responsehistoryheader')) . 493 $options->extrahistorycontent . 494 html_writer::tag('div', html_writer::table($table, true), 495 array('class' => 'responsehistoryheader')); 496 } 497 498 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body