Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 $divattributes = array('class' => 'questionflag'); 267 268 switch ($flagsoption) { 269 case question_display_options::VISIBLE: 270 $flagcontent = $this->get_flag_html($qa->is_flagged()); 271 break; 272 273 case question_display_options::EDITABLE: 274 $id = $qa->get_flag_field_name(); 275 // The checkbox id must be different from any element name, because 276 // of a stupid IE bug: 277 // http://www.456bereastreet.com/archive/200802/beware_of_id_and_name_attribute_mixups_when_using_getelementbyid_in_internet_explorer/ 278 $checkboxattributes = array( 279 'type' => 'checkbox', 280 'id' => $id . 'checkbox', 281 'name' => $id, 282 'value' => 1, 283 ); 284 if ($qa->is_flagged()) { 285 $checkboxattributes['checked'] = 'checked'; 286 } 287 $postdata = question_flags::get_postdata($qa); 288 289 $flagcontent = html_writer::empty_tag('input', 290 array('type' => 'hidden', 'name' => $id, 'value' => 0)) . 291 html_writer::empty_tag('input', 292 array('type' => 'hidden', 'value' => $postdata, 'class' => 'questionflagpostdata')) . 293 html_writer::empty_tag('input', $checkboxattributes) . 294 html_writer::tag('label', $this->get_flag_html($qa->is_flagged(), $id . 'img'), 295 array('id' => $id . 'label', 'for' => $id . 'checkbox')) . "\n"; 296 297 $divattributes = array( 298 'class' => 'questionflag editable', 299 ); 300 301 break; 302 303 default: 304 $flagcontent = ''; 305 } 306 307 return html_writer::nonempty_tag('div', $flagcontent, $divattributes); 308 } 309 310 /** 311 * Work out the actual img tag needed for the flag 312 * 313 * @param bool $flagged whether the question is currently flagged. 314 * @param string $id an id to be added as an attribute to the img (optional). 315 * @return string the img tag. 316 */ 317 protected function get_flag_html($flagged, $id = '') { 318 if ($flagged) { 319 $icon = 'i/flagged'; 320 $label = get_string('clickunflag', 'question'); 321 } else { 322 $icon = 'i/unflagged'; 323 $label = get_string('clickflag', 'question'); 324 } 325 $attributes = [ 326 'src' => $this->image_url($icon), 327 'alt' => '', 328 'class' => 'questionflagimage', 329 ]; 330 if ($id) { 331 $attributes['id'] = $id; 332 } 333 $img = html_writer::empty_tag('img', $attributes); 334 $img .= html_writer::span($label); 335 336 return $img; 337 } 338 339 /** 340 * Generate the display of the edit question link. 341 * 342 * @param question_attempt $qa The question attempt to display. 343 * @param question_display_options $options controls what should and should not be displayed. 344 * @return string 345 */ 346 protected function edit_question_link(question_attempt $qa, question_display_options $options) { 347 if (empty($options->editquestionparams)) { 348 return ''; 349 } 350 351 $params = $options->editquestionparams; 352 if ($params['returnurl'] instanceof moodle_url) { 353 $params['returnurl'] = $params['returnurl']->out_as_local_url(false); 354 } 355 $params['id'] = $qa->get_question_id(); 356 $editurl = new moodle_url('/question/question.php', $params); 357 358 return html_writer::tag('div', html_writer::link( 359 $editurl, $this->pix_icon('t/edit', get_string('edit'), '', array('class' => 'iconsmall')) . 360 get_string('editquestion', 'question')), 361 array('class' => 'editquestion')); 362 } 363 364 /** 365 * Generate the display of the formulation part of the question. This is the 366 * area that contains the quetsion text, and the controls for students to 367 * input their answers. Some question types also embed feedback, for 368 * example ticks and crosses, in this area. 369 * 370 * @param question_attempt $qa the question attempt to display. 371 * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour 372 * specific parts. 373 * @param qtype_renderer $qtoutput the renderer to output the question type 374 * specific parts. 375 * @param question_display_options $options controls what should and should not be displayed. 376 * @return HTML fragment. 377 */ 378 protected function formulation(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 379 qtype_renderer $qtoutput, question_display_options $options) { 380 $output = ''; 381 $output .= html_writer::empty_tag('input', array( 382 'type' => 'hidden', 383 'name' => $qa->get_control_field_name('sequencecheck'), 384 'value' => $qa->get_sequence_check_count())); 385 $output .= $qtoutput->formulation_and_controls($qa, $options); 386 if ($options->clearwrong) { 387 $output .= $qtoutput->clear_wrong($qa); 388 } 389 $output .= html_writer::nonempty_tag('div', 390 $behaviouroutput->controls($qa, $options), array('class' => 'im-controls')); 391 return $output; 392 } 393 394 /** 395 * Generate the display of the outcome part of the question. This is the 396 * area that contains the various forms of feedback. 397 * 398 * @param question_attempt $qa the question attempt to display. 399 * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour 400 * specific parts. 401 * @param qtype_renderer $qtoutput the renderer to output the question type 402 * specific parts. 403 * @param question_display_options $options controls what should and should not be displayed. 404 * @return HTML fragment. 405 */ 406 protected function outcome(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 407 qtype_renderer $qtoutput, question_display_options $options) { 408 $output = ''; 409 $output .= html_writer::nonempty_tag('div', 410 $qtoutput->feedback($qa, $options), array('class' => 'feedback')); 411 $output .= html_writer::nonempty_tag('div', 412 $behaviouroutput->feedback($qa, $options), array('class' => 'im-feedback')); 413 $output .= html_writer::nonempty_tag('div', 414 $options->extrainfocontent, array('class' => 'extra-feedback')); 415 return $output; 416 } 417 418 protected function manual_comment(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 419 qtype_renderer $qtoutput, question_display_options $options) { 420 return $qtoutput->manual_comment($qa, $options) . 421 $behaviouroutput->manual_comment($qa, $options); 422 } 423 424 /** 425 * Generate the display of the response history part of the question. This 426 * is the table showing all the steps the question has been through. 427 * 428 * @param question_attempt $qa the question attempt to display. 429 * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour 430 * specific parts. 431 * @param qtype_renderer $qtoutput the renderer to output the question type 432 * specific parts. 433 * @param question_display_options $options controls what should and should not be displayed. 434 * @return HTML fragment. 435 */ 436 protected function response_history(question_attempt $qa, qbehaviour_renderer $behaviouroutput, 437 qtype_renderer $qtoutput, question_display_options $options) { 438 439 if (!$options->history) { 440 return ''; 441 } 442 443 $table = new html_table(); 444 $table->head = array ( 445 get_string('step', 'question'), 446 get_string('time'), 447 get_string('action', 'question'), 448 get_string('state', 'question'), 449 ); 450 if ($options->marks >= question_display_options::MARK_AND_MAX) { 451 $table->head[] = get_string('marks', 'question'); 452 } 453 454 foreach ($qa->get_full_step_iterator() as $i => $step) { 455 $stepno = $i + 1; 456 457 $rowclass = ''; 458 if ($stepno == $qa->get_num_steps()) { 459 $rowclass = 'current'; 460 } else if (!empty($options->questionreviewlink)) { 461 $url = new moodle_url($options->questionreviewlink, 462 array('slot' => $qa->get_slot(), 'step' => $i)); 463 $stepno = $this->output->action_link($url, $stepno, 464 new popup_action('click', $url, 'reviewquestion', 465 array('width' => 450, 'height' => 650)), 466 array('title' => get_string('reviewresponse', 'question'))); 467 } 468 469 $restrictedqa = new question_attempt_with_restricted_history($qa, $i, null); 470 471 $row = [$stepno, 472 userdate($step->get_timecreated(), get_string('strftimedatetimeshort')), 473 s($qa->summarise_action($step)) . $this->action_author($step, $options), 474 $restrictedqa->get_state_string($options->correctness)]; 475 476 if ($options->marks >= question_display_options::MARK_AND_MAX) { 477 $row[] = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp); 478 } 479 480 $table->rowclasses[] = $rowclass; 481 $table->data[] = $row; 482 } 483 484 return html_writer::tag('h4', get_string('responsehistory', 'question'), 485 array('class' => 'responsehistoryheader')) . 486 $options->extrahistorycontent . 487 html_writer::tag('div', html_writer::table($table, true), 488 array('class' => 'responsehistoryheader')); 489 } 490 491 /** 492 * Action author's profile link. 493 * 494 * @param question_attempt_step $step The step. 495 * @param question_display_options $options The display options. 496 * @return string The link to user's profile. 497 */ 498 protected function action_author(question_attempt_step $step, question_display_options $options): string { 499 if ($options->userinfoinhistory && $step->get_user_id() != $options->userinfoinhistory) { 500 return html_writer::link( 501 new moodle_url('/user/view.php', ['id' => $step->get_user_id(), 'course' => $this->page->course->id]), 502 $step->get_user_fullname(), ['class' => 'd-table-cell']); 503 } else { 504 return ''; 505 } 506 } 507 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body