1<?php 2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */ 3 4require_once './Modules/TestQuestionPool/classes/class.assQuestionGUI.php'; 5require_once './Modules/TestQuestionPool/interfaces/interface.ilGuiQuestionScoringAdjustable.php'; 6require_once './Modules/TestQuestionPool/interfaces/interface.ilGuiAnswerScoringAdjustable.php'; 7require_once './Modules/Test/classes/inc.AssessmentConstants.php'; 8 9/** 10 * Numeric question GUI representation 11 * 12 * The assNumericGUI class encapsulates the GUI representation 13 * for numeric questions. 14 * 15 * @author Helmut Schottmüller <helmut.schottmueller@mac.com> 16 * @author Nina Gharib <nina@wgserve.de> 17 * @author Björn Heyser <bheyser@databay.de> 18 * @author Maximilian Becker <mbecker@databay.de> 19 * 20 * @version $Id$ 21 * 22 * @ingroup ModulesTestQuestionPool 23 * @ilCtrl_Calls assNumericGUI: ilFormPropertyDispatchGUI 24 */ 25class assNumericGUI extends assQuestionGUI implements ilGuiQuestionScoringAdjustable, ilGuiAnswerScoringAdjustable 26{ 27 /** 28 * assNumericGUI constructor 29 * 30 * The constructor takes possible arguments an creates an instance of the assNumericGUI object. 31 * 32 * @param integer $id The database id of a Numeric question object 33 * 34 * @return assNumericGUI 35 */ 36 public function __construct($id = -1) 37 { 38 parent::__construct(); 39 require_once './Modules/TestQuestionPool/classes/class.assNumeric.php'; 40 $this->object = new assNumeric(); 41 if ($id >= 0) { 42 $this->object->loadFromDb($id); 43 } 44 } 45 46 public function getCommand($cmd) 47 { 48 if (substr($cmd, 0, 6) == "delete") { 49 $cmd = "delete"; 50 } 51 return $cmd; 52 } 53 54 /** 55 * {@inheritdoc} 56 */ 57 protected function writePostData($always = false) 58 { 59 $hasErrors = (!$always) ? $this->editQuestion(true) : false; 60 if (!$hasErrors) { 61 require_once 'Services/Form/classes/class.ilPropertyFormGUI.php'; 62 $this->writeQuestionGenericPostData(); 63 $this->writeQuestionSpecificPostData(new ilPropertyFormGUI()); 64 $this->writeAnswerSpecificPostData(new ilPropertyFormGUI()); 65 $this->saveTaxonomyAssignments(); 66 return 0; 67 } 68 return 1; 69 } 70 71 /** 72 * Creates an output of the edit form for the question 73 * 74 * @param bool $checkonly 75 * 76 * @return bool 77 */ 78 public function editQuestion($checkonly = false) 79 { 80 $save = $this->isSaveCommand(); 81 $this->getQuestionTemplate(); 82 83 include_once("./Services/Form/classes/class.ilPropertyFormGUI.php"); 84 $form = new ilPropertyFormGUI(); 85 $this->editForm = $form; 86 87 $form->setFormAction($this->ctrl->getFormAction($this)); 88 $form->setTitle($this->outQuestionType()); 89 $form->setMultipart(true); 90 $form->setTableWidth("100%"); 91 $form->setId("assnumeric"); 92 93 $this->addBasicQuestionFormProperties($form); 94 $this->populateQuestionSpecificFormPart($form); 95 $this->populateAnswerSpecificFormPart($form); 96 $this->populateTaxonomyFormSection($form); 97 $this->addQuestionFormCommandButtons($form); 98 99 $errors = false; 100 101 if ($save) { 102 $form->setValuesByPost(); 103 $errors = !$form->checkInput(); 104 $form->setValuesByPost(); // again, because checkInput now performs the whole stripSlashes handling and we need this if we don't want to have duplication of backslashes 105 106 $lower = $form->getItemByPostVar('lowerlimit'); 107 $upper = $form->getItemByPostVar('upperlimit'); 108 109 if (!$this->checkRange($lower->getValue(), $upper->getValue())) { 110 global $DIC; 111 $lower->setAlert($DIC->language()->txt('qpl_numeric_lower_needs_valid_lower_alert')); 112 $upper->setAlert($DIC->language()->txt('qpl_numeric_upper_needs_valid_upper_alert')); 113 ilUtil::sendFailure($DIC->language()->txt("form_input_not_valid")); 114 $errors = true; 115 } 116 117 if ($errors) { 118 $checkonly = false; 119 } 120 } 121 122 if (!$checkonly) { 123 $this->tpl->setVariable("QUESTION_DATA", $form->getHTML()); 124 } 125 return $errors; 126 } 127 128 /** 129 * Checks the range limits 130 * 131 * Checks the Range limits Upper and Lower for their correctness 132 * 133 * @return boolean 134 */ 135 public function checkRange($lower, $upper) 136 { 137 include_once "./Services/Math/classes/class.EvalMath.php"; 138 $eval = new EvalMath(); 139 $eval->suppress_errors = true; 140 if (($eval->e($lower) !== false) and ($eval->e($upper) !== false)) { 141 if ($eval->e($lower) <= $eval->e($upper)) { 142 return true; 143 } else { 144 return false; 145 } 146 } 147 return false; 148 } 149 150 /** 151 * Question type specific support of intermediate solution output 152 * The function getSolutionOutput respects getUseIntermediateSolution() 153 * @return bool 154 */ 155 public function supportsIntermediateSolutionOutput() 156 { 157 return true; 158 } 159 160 /** 161 * Get the question solution output 162 * 163 * @param integer $active_id The active user id 164 * @param integer $pass The test pass 165 * @param boolean $graphicalOutput Show visual feedback for right/wrong answers 166 * @param boolean $result_output Show the reached points for parts of the question 167 * @param boolean $show_question_only Show the question without the ILIAS content around 168 * @param boolean $show_feedback Show the question feedback 169 * @param boolean $show_correct_solution Show the correct solution instead of the user solution 170 * @param boolean $show_manual_scoring Show specific information for the manual scoring output 171 * @param bool $show_question_text 172 * 173 * @return string The solution output of the question as HTML code 174 */ 175 public function getSolutionOutput( 176 $active_id, 177 $pass = null, 178 $graphicalOutput = false, 179 $result_output = false, 180 $show_question_only = true, 181 $show_feedback = false, 182 $show_correct_solution = false, 183 $show_manual_scoring = false, 184 $show_question_text = true 185 ) { 186 // get the solution of the user for the active pass or from the last pass if allowed 187 $solutions = array(); 188 if (($active_id > 0) && (!$show_correct_solution)) { 189 $solutions = $this->object->getSolutionValues($active_id, $pass, !$this->getUseIntermediateSolution()); 190 } else { 191 array_push($solutions, array("value1" => sprintf($this->lng->txt("value_between_x_and_y"), $this->object->getLowerLimit(), $this->object->getUpperLimit()))); 192 } 193 194 // generate the question output 195 require_once './Services/UICore/classes/class.ilTemplate.php'; 196 $template = new ilTemplate("tpl.il_as_qpl_numeric_output_solution.html", true, true, "Modules/TestQuestionPool"); 197 $solutiontemplate = new ilTemplate("tpl.il_as_tst_solution_output.html", true, true, "Modules/TestQuestionPool"); 198 if (is_array($solutions)) { 199 if (($active_id > 0) && (!$show_correct_solution)) { 200 if ($graphicalOutput) { 201 if ($this->object->getStep() === null) { 202 $reached_points = $this->object->getReachedPoints($active_id, $pass); 203 } else { 204 $reached_points = $this->object->calculateReachedPoints($active_id, $pass); 205 } 206 // output of ok/not ok icons for user entered solutions 207 if ($reached_points == $this->object->getMaximumPoints()) { 208 $template->setCurrentBlock("icon_ok"); 209 $template->setVariable("ICON_OK", ilUtil::getImagePath("icon_ok.svg")); 210 $template->setVariable("TEXT_OK", $this->lng->txt("answer_is_right")); 211 $template->parseCurrentBlock(); 212 } else { 213 $template->setCurrentBlock("icon_ok"); 214 $template->setVariable("ICON_NOT_OK", ilUtil::getImagePath("icon_not_ok.svg")); 215 $template->setVariable("TEXT_NOT_OK", $this->lng->txt("answer_is_wrong")); 216 $template->parseCurrentBlock(); 217 } 218 } 219 } 220 foreach ($solutions as $solution) { 221 $template->setVariable("NUMERIC_VALUE", $solution["value1"]); 222 } 223 if (count($solutions) == 0) { 224 $template->setVariable("NUMERIC_VALUE", " "); 225 } 226 } 227 $template->setVariable("NUMERIC_SIZE", $this->object->getMaxChars()); 228 $questiontext = $this->object->getQuestion(); 229 if ($show_question_text == true) { 230 $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, true)); 231 } 232 $questionoutput = $template->get(); 233 //$feedback = ($show_feedback) ? $this->getAnswerFeedbackOutput($active_id, $pass) : ""; // Moving new method 234 // due to deprecation. 235 $feedback = ($show_feedback && !$this->isTestPresentationContext()) ? $this->getGenericFeedbackOutput($active_id, $pass) : ""; 236 if (strlen($feedback)) { 237 $cssClass = ( 238 $this->hasCorrectSolution($active_id, $pass) ? 239 ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_CORRECT : ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_WRONG 240 ); 241 242 $solutiontemplate->setVariable("ILC_FB_CSS_CLASS", $cssClass); 243 $solutiontemplate->setVariable("FEEDBACK", $this->object->prepareTextareaOutput($feedback, true)); 244 } 245 $solutiontemplate->setVariable("SOLUTION_OUTPUT", $questionoutput); 246 247 $solutionoutput = $solutiontemplate->get(); 248 if (!$show_question_only) { 249 // get page object output 250 $solutionoutput = $this->getILIASPage($solutionoutput); 251 } 252 return $solutionoutput; 253 } 254 255 /** 256 * @param bool $show_question_only 257 * 258 * @return string 259 */ 260 public function getPreview($show_question_only = false, $showInlineFeedback = false) 261 { 262 // generate the question output 263 require_once './Services/UICore/classes/class.ilTemplate.php'; 264 $template = new ilTemplate("tpl.il_as_qpl_numeric_output.html", true, true, "Modules/TestQuestionPool"); 265 if (is_object($this->getPreviewSession())) { 266 $template->setVariable("NUMERIC_VALUE", " value=\"" . $this->getPreviewSession()->getParticipantsSolution() . "\""); 267 } 268 $template->setVariable("NUMERIC_SIZE", $this->object->getMaxChars()); 269 $questiontext = $this->object->getQuestion(); 270 $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, true)); 271 $questionoutput = $template->get(); 272 if (!$show_question_only) { 273 // get page object output 274 $questionoutput = $this->getILIASPage($questionoutput); 275 } 276 return $questionoutput; 277 } 278 279 /** 280 * @param integer $active_id 281 * @param integer|null $pass 282 * @param bool $is_postponed 283 * @param bool $use_post_solutions 284 * 285 * @return string 286 */ 287 // hey: prevPassSolutions - pass will be always available from now on 288 public function getTestOutput($active_id, $pass, $is_postponed = false, $use_post_solutions = false, $inlineFeedback) 289 // hey. 290 { 291 $solutions = null; 292 // get the solution of the user for the active pass or from the last pass if allowed 293 if ($use_post_solutions !== false) { 294 $solutions = array( 295 array('value1' => $use_post_solutions['numeric_result']) 296 ); 297 } elseif ($active_id) { 298 299 // hey: prevPassSolutions - obsolete due to central check 300 #include_once "./Modules/Test/classes/class.ilObjTest.php"; 301 #if (!ilObjTest::_getUsePreviousAnswers($active_id, true)) 302 #{ 303 # if (is_null($pass)) $pass = ilObjTest::_getPass($active_id); 304 #} 305 $solutions = $this->object->getTestOutputSolutions($active_id, $pass); 306 // hey. 307 } 308 309 // generate the question output 310 require_once './Services/UICore/classes/class.ilTemplate.php'; 311 $template = new ilTemplate("tpl.il_as_qpl_numeric_output.html", true, true, "Modules/TestQuestionPool"); 312 if (is_array($solutions)) { 313 foreach ($solutions as $solution) { 314 $template->setVariable("NUMERIC_VALUE", " value=\"" . $solution["value1"] . "\""); 315 } 316 } 317 $template->setVariable("NUMERIC_SIZE", $this->object->getMaxChars()); 318 $questiontext = $this->object->getQuestion(); 319 $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, true)); 320 $questionoutput = $template->get(); 321 $pageoutput = $this->outQuestionPage("", $is_postponed, $active_id, $questionoutput); 322 return $pageoutput; 323 } 324 325 /** 326 * Sets the ILIAS tabs for this question type 327 * 328 * @todo: MOVE THIS STEPS TO COMMON QUESTION CLASS assQuestionGUI 329 */ 330 public function setQuestionTabs() 331 { 332 /** @var $rbacsystem ilRbacSystem */ 333 /** @var $ilTabs ilTabsGUI */ 334 global $DIC; 335 $rbacsystem = $DIC['rbacsystem']; 336 $ilTabs = $DIC['ilTabs']; 337 338 $ilTabs->clearTargets(); 339 340 $this->ctrl->setParameterByClass("ilAssQuestionPageGUI", "q_id", $_GET["q_id"]); 341 include_once "./Modules/TestQuestionPool/classes/class.assQuestion.php"; 342 $q_type = $this->object->getQuestionType(); 343 344 if (strlen($q_type)) { 345 $classname = $q_type . "GUI"; 346 $this->ctrl->setParameterByClass(strtolower($classname), "sel_question_types", $q_type); 347 $this->ctrl->setParameterByClass(strtolower($classname), "q_id", $_GET["q_id"]); 348 } 349 350 if ($_GET["q_id"]) { 351 if ($rbacsystem->checkAccess('write', $_GET["ref_id"])) { 352 // edit page 353 $ilTabs->addTarget( 354 "edit_page", 355 $this->ctrl->getLinkTargetByClass("ilAssQuestionPageGUI", "edit"), 356 array("edit", "insert", "exec_pg"), 357 "", 358 "", 359 $force_active 360 ); 361 } 362 363 $this->addTab_QuestionPreview($ilTabs); 364 } 365 366 $force_active = false; 367 if ($rbacsystem->checkAccess('write', $_GET["ref_id"])) { 368 $url = ""; 369 if ($classname) { 370 $url = $this->ctrl->getLinkTargetByClass($classname, "editQuestion"); 371 } 372 // edit question properties 373 $ilTabs->addTarget( 374 "edit_question", 375 $url, 376 array("editQuestion", "save", "cancel", "saveEdit", "originalSyncForm"), 377 $classname, 378 "", 379 $force_active 380 ); 381 } 382 383 // add tab for question feedback within common class assQuestionGUI 384 $this->addTab_QuestionFeedback($ilTabs); 385 386 // add tab for question hint within common class assQuestionGUI 387 $this->addTab_QuestionHints($ilTabs); 388 389 // add tab for question's suggested solution within common class assQuestionGUI 390 $this->addTab_SuggestedSolution($ilTabs, $classname); 391 392 // Assessment of questions sub menu entry 393 if ($_GET["q_id"]) { 394 $ilTabs->addTarget( 395 "statistics", 396 $this->ctrl->getLinkTargetByClass($classname, "assessment"), 397 array("assessment"), 398 $classname, 399 "" 400 ); 401 } 402 403 $this->addBackTab($ilTabs); 404 } 405 406 /** 407 * @param int $active_id 408 * @param int $pass 409 * 410 * @return mixed|string 411 */ 412 public function getSpecificFeedbackOutput($userSolution) 413 { 414 $output = ""; 415 return $this->object->prepareTextareaOutput($output, true); 416 } 417 418 public function writeQuestionSpecificPostData(ilPropertyFormGUI $form) 419 { 420 $this->object->setMaxChars($_POST["maxchars"]); 421 } 422 423 public function writeAnswerSpecificPostData(ilPropertyFormGUI $form) 424 { 425 $this->object->setLowerLimit($_POST['lowerlimit']); 426 $this->object->setUpperLimit($_POST['upperlimit']); 427 $this->object->setPoints($_POST['points']); 428 } 429 430 public function populateQuestionSpecificFormPart(\ilPropertyFormGUI $form) 431 { 432 // maxchars 433 $maxchars = new ilNumberInputGUI($this->lng->txt("maxchars"), "maxchars"); 434 $maxchars->setInfo($this->lng->txt('qpl_maxchars_info_numeric_question')); 435 $maxchars->setSize(10); 436 $maxchars->setDecimals(0); 437 $maxchars->setMinValue(1); 438 $maxchars->setRequired(true); 439 if ($this->object->getMaxChars() > 0) { 440 $maxchars->setValue($this->object->getMaxChars()); 441 } 442 $form->addItem($maxchars); 443 } 444 445 public function populateAnswerSpecificFormPart(\ilPropertyFormGUI $form) 446 { 447 // points 448 $points = new ilNumberInputGUI($this->lng->txt("points"), "points"); 449 $points->allowDecimals(true); 450 $points->setValue($this->object->getPoints() > 0 ? $this->object->getPoints() : ''); 451 $points->setRequired(true); 452 $points->setSize(3); 453 $points->setMinValue(0.0); 454 $points->setMinvalueShouldBeGreater(true); 455 $form->addItem($points); 456 457 $header = new ilFormSectionHeaderGUI(); 458 $header->setTitle($this->lng->txt("range")); 459 $form->addItem($header); 460 461 // lower bound 462 $lower_limit = new ilFormulaInputGUI($this->lng->txt("range_lower_limit"), "lowerlimit"); 463 $lower_limit->setSize(25); 464 $lower_limit->setMaxLength(20); 465 $lower_limit->setRequired(true); 466 $lower_limit->setValue($this->object->getLowerLimit()); 467 $form->addItem($lower_limit); 468 469 // upper bound 470 $upper_limit = new ilFormulaInputGUI($this->lng->txt("range_upper_limit"), "upperlimit"); 471 $upper_limit->setSize(25); 472 $upper_limit->setMaxLength(20); 473 $upper_limit->setRequired(true); 474 $upper_limit->setValue($this->object->getUpperLimit()); 475 $form->addItem($upper_limit); 476 477 // reset input length, if max chars are set 478 if ($this->object->getMaxChars() > 0) { 479 $lower_limit->setSize($this->object->getMaxChars()); 480 $lower_limit->setMaxLength($this->object->getMaxChars()); 481 $upper_limit->setSize($this->object->getMaxChars()); 482 $upper_limit->setMaxLength($this->object->getMaxChars()); 483 } 484 } 485 486 /** 487 * Returns a list of postvars which will be suppressed in the form output when used in scoring adjustment. 488 * The form elements will be shown disabled, so the users see the usual form but can only edit the settings, which 489 * make sense in the given context. 490 * 491 * E.g. array('cloze_type', 'image_filename') 492 * 493 * @return string[] 494 */ 495 public function getAfterParticipationSuppressionAnswerPostVars() 496 { 497 return array(); 498 } 499 500 /** 501 * Returns a list of postvars which will be suppressed in the form output when used in scoring adjustment. 502 * The form elements will be shown disabled, so the users see the usual form but can only edit the settings, which 503 * make sense in the given context. 504 * 505 * E.g. array('cloze_type', 'image_filename') 506 * 507 * @return string[] 508 */ 509 public function getAfterParticipationSuppressionQuestionPostVars() 510 { 511 return array(); 512 } 513 514 /** 515 * Returns an html string containing a question specific representation of the answers so far 516 * given in the test for use in the right column in the scoring adjustment user interface. 517 * 518 * @param array $relevant_answers 519 * 520 * @return string 521 */ 522 public function getAggregatedAnswersView($relevant_answers) 523 { 524 return $this->renderAggregateView( 525 $this->aggregateAnswers($relevant_answers) 526 )->get(); 527 } 528 529 public function aggregateAnswers($relevant_answers_chosen) 530 { 531 $aggregate = array(); 532 533 foreach ($relevant_answers_chosen as $relevant_answer) { 534 if (array_key_exists($relevant_answer['value1'], $aggregate)) { 535 $aggregate[$relevant_answer['value1']]++; 536 } else { 537 $aggregate[$relevant_answer['value1']] = 1; 538 } 539 } 540 return $aggregate; 541 } 542 543 /** 544 * @param $aggregate 545 * 546 * @return ilTemplate 547 */ 548 public function renderAggregateView($aggregate) 549 { 550 $tpl = new ilTemplate('tpl.il_as_aggregated_answers_table.html', true, true, "Modules/TestQuestionPool"); 551 552 $tpl->setCurrentBlock('headercell'); 553 $tpl->setVariable('HEADER', $this->lng->txt('tst_answer_aggr_answer_header')); 554 $tpl->parseCurrentBlock(); 555 556 $tpl->setCurrentBlock('headercell'); 557 $tpl->setVariable('HEADER', $this->lng->txt('tst_answer_aggr_frequency_header')); 558 $tpl->parseCurrentBlock(); 559 560 foreach ($aggregate as $key => $value) { 561 $tpl->setCurrentBlock('aggregaterow'); 562 $tpl->setVariable('OPTION', $key); 563 $tpl->setVariable('COUNT', $value); 564 $tpl->parseCurrentBlock(); 565 } 566 return $tpl; 567 } 568 569 public function getAnswersFrequency($relevantAnswers, $questionIndex) 570 { 571 $answers = array(); 572 573 foreach ($relevantAnswers as $ans) { 574 if (!isset($answers[$ans['value1']])) { 575 $answers[$ans['value1']] = array( 576 'answer' => $ans['value1'], 'frequency' => 0 577 ); 578 } 579 580 $answers[$ans['value1']]['frequency']++; 581 } 582 583 return $answers; 584 } 585 586 public function populateCorrectionsFormProperties(ilPropertyFormGUI $form) 587 { 588 // points 589 $points = new ilNumberInputGUI($this->lng->txt("points"), "points"); 590 $points->allowDecimals(true); 591 $points->setValue($this->object->getPoints() > 0 ? $this->object->getPoints() : ''); 592 $points->setRequired(true); 593 $points->setSize(3); 594 $points->setMinValue(0.0); 595 $points->setMinvalueShouldBeGreater(true); 596 $form->addItem($points); 597 598 $header = new ilFormSectionHeaderGUI(); 599 $header->setTitle($this->lng->txt("range")); 600 $form->addItem($header); 601 602 // lower bound 603 $lower_limit = new ilFormulaInputGUI($this->lng->txt("range_lower_limit"), "lowerlimit"); 604 $lower_limit->setSize(25); 605 $lower_limit->setMaxLength(20); 606 $lower_limit->setRequired(true); 607 $lower_limit->setValue($this->object->getLowerLimit()); 608 $form->addItem($lower_limit); 609 610 // upper bound 611 $upper_limit = new ilFormulaInputGUI($this->lng->txt("range_upper_limit"), "upperlimit"); 612 $upper_limit->setSize(25); 613 $upper_limit->setMaxLength(20); 614 $upper_limit->setRequired(true); 615 $upper_limit->setValue($this->object->getUpperLimit()); 616 $form->addItem($upper_limit); 617 618 // reset input length, if max chars are set 619 if ($this->object->getMaxChars() > 0) { 620 $lower_limit->setSize($this->object->getMaxChars()); 621 $lower_limit->setMaxLength($this->object->getMaxChars()); 622 $upper_limit->setSize($this->object->getMaxChars()); 623 $upper_limit->setMaxLength($this->object->getMaxChars()); 624 } 625 } 626 627 /** 628 * @param ilPropertyFormGUI $form 629 */ 630 public function saveCorrectionsFormProperties(ilPropertyFormGUI $form) 631 { 632 $this->object->setPoints((float) $form->getInput('points')); 633 $this->object->setLowerLimit((float) $form->getInput('lowerlimit')); 634 $this->object->setUpperLimit((float) $form->getInput('upperlimit')); 635 } 636} 637