1<?php 2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */ 3 4require_once 'Modules/TestQuestionPool/classes/class.assQuestion.php'; 5require_once 'Modules/TestQuestionPool/interfaces/interface.ilObjQuestionScoringAdjustable.php'; 6require_once 'Modules/TestQuestionPool/interfaces/interface.ilObjAnswerScoringAdjustable.php'; 7require_once 'Modules/TestQuestionPool/interfaces/interface.ilAssSpecificFeedbackOptionLabelProvider.php'; 8 9/** 10 * @author Björn Heyser <bheyser@databay.de> 11 * @version $Id$ 12 * 13 * @package Modules/TestQuestionPool 14 */ 15class assKprimChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, ilAssSpecificFeedbackOptionLabelProvider 16{ 17 const NUM_REQUIRED_ANSWERS = 4; 18 19 const PARTIAL_SCORING_NUM_CORRECT_ANSWERS = 3; 20 21 const ANSWER_TYPE_SINGLE_LINE = 'singleLine'; 22 const ANSWER_TYPE_MULTI_LINE = 'multiLine'; 23 24 const OPTION_LABEL_RIGHT_WRONG = 'right_wrong'; 25 const OPTION_LABEL_PLUS_MINUS = 'plus_minus'; 26 const OPTION_LABEL_APPLICABLE_OR_NOT = 'applicable_or_not'; 27 const OPTION_LABEL_ADEQUATE_OR_NOT = 'adequate_or_not'; 28 const OPTION_LABEL_CUSTOM = 'customlabel'; 29 30 const DEFAULT_THUMB_SIZE = 150; 31 const THUMB_PREFIX = 'thumb.'; 32 33 private $shuffleAnswersEnabled; 34 35 private $answerType; 36 37 private $thumbSize; 38 39 private $scorePartialSolutionEnabled; 40 41 private $optionLabel; 42 43 private $customTrueOptionLabel; 44 45 private $customFalseOptionLabel; 46 47 private $specificFeedbackSetting; 48 49 private $answers; 50 51 public function __construct($title = '', $comment = '', $author = '', $owner = -1, $question = '') 52 { 53 parent::__construct($title, $comment, $author, $owner, $question); 54 55 $this->shuffleAnswersEnabled = true; 56 $this->answerType = self::ANSWER_TYPE_SINGLE_LINE; 57 $this->thumbSize = self::DEFAULT_THUMB_SIZE; 58 $this->scorePartialSolutionEnabled = true; 59 $this->optionLabel = self::OPTION_LABEL_RIGHT_WRONG; 60 $this->customTrueOptionLabel = ''; 61 $this->customFalseOptionLabel = ''; 62 63 require_once 'Modules/TestQuestionPool/classes/feedback/class.ilAssConfigurableMultiOptionQuestionFeedback.php'; 64 $this->specificFeedbackSetting = ilAssConfigurableMultiOptionQuestionFeedback::FEEDBACK_SETTING_ALL; 65 66 $this->answers = array(); 67 68 $this->setPoints(''); 69 } 70 71 public function getQuestionType() 72 { 73 return 'assKprimChoice'; 74 } 75 76 public function getAdditionalTableName() 77 { 78 return "qpl_qst_kprim"; 79 } 80 81 public function getAnswerTableName() 82 { 83 return "qpl_a_kprim"; 84 } 85 86 public function setShuffleAnswersEnabled($shuffleAnswersEnabled) 87 { 88 $this->shuffleAnswersEnabled = $shuffleAnswersEnabled; 89 } 90 91 public function isShuffleAnswersEnabled() 92 { 93 return $this->shuffleAnswersEnabled; 94 } 95 96 public function setAnswerType($answerType) 97 { 98 $this->answerType = $answerType; 99 } 100 101 public function getAnswerType() 102 { 103 return $this->answerType; 104 } 105 106 public function setThumbSize($thumbSize) 107 { 108 $this->thumbSize = $thumbSize; 109 } 110 111 public function getThumbSize() 112 { 113 return $this->thumbSize; 114 } 115 116 public function setScorePartialSolutionEnabled($scorePartialSolutionEnabled) 117 { 118 $this->scorePartialSolutionEnabled = $scorePartialSolutionEnabled; 119 } 120 121 public function isScorePartialSolutionEnabled() 122 { 123 return $this->scorePartialSolutionEnabled; 124 } 125 126 public function setOptionLabel($optionLabel) 127 { 128 $this->optionLabel = $optionLabel; 129 } 130 131 public function getOptionLabel() 132 { 133 return $this->optionLabel; 134 } 135 136 public function setCustomTrueOptionLabel($customTrueOptionLabel) 137 { 138 $this->customTrueOptionLabel = $customTrueOptionLabel; 139 } 140 141 public function getCustomTrueOptionLabel() 142 { 143 return $this->customTrueOptionLabel; 144 } 145 146 public function setCustomFalseOptionLabel($customFalseOptionLabel) 147 { 148 $this->customFalseOptionLabel = $customFalseOptionLabel; 149 } 150 151 public function getCustomFalseOptionLabel() 152 { 153 return $this->customFalseOptionLabel; 154 } 155 156 public function setSpecificFeedbackSetting($specificFeedbackSetting) 157 { 158 $this->specificFeedbackSetting = $specificFeedbackSetting; 159 } 160 161 public function getSpecificFeedbackSetting() 162 { 163 return $this->specificFeedbackSetting; 164 } 165 166 public function setAnswers($answers) 167 { 168 $this->answers = $answers; 169 } 170 171 public function getAnswers() 172 { 173 return $this->answers; 174 } 175 176 public function getAnswer($position) 177 { 178 foreach ($this->getAnswers() as $answer) { 179 if ($answer->getPosition() == $position) { 180 return $answer; 181 } 182 } 183 184 return null; 185 } 186 187 public function addAnswer(ilAssKprimChoiceAnswer $answer) 188 { 189 $this->answers[] = $answer; 190 } 191 192 public function loadFromDb($questionId) 193 { 194 $res = $this->db->queryF($this->buildQuestionDataQuery(), array('integer'), array($questionId)); 195 196 while ($data = $this->db->fetchAssoc($res)) { 197 $this->setId($questionId); 198 199 $this->setOriginalId($data['original_id']); 200 201 $this->setObjId($data['obj_fi']); 202 203 $this->setTitle($data['title']); 204 $this->setNrOfTries($data['nr_of_tries']); 205 $this->setComment($data['description']); 206 $this->setAuthor($data['author']); 207 $this->setPoints($data['points']); 208 $this->setOwner($data['owner']); 209 $this->setEstimatedWorkingTimeFromDurationString($data['working_time']); 210 $this->setLastChange($data['tstamp']); 211 require_once 'Services/RTE/classes/class.ilRTE.php'; 212 $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($data['question_text'], 1)); 213 214 $this->setShuffleAnswersEnabled((bool) $data['shuffle_answers']); 215 216 if ($this->isValidAnswerType($data['answer_type'])) { 217 $this->setAnswerType($data['answer_type']); 218 } 219 220 if (is_numeric($data['thumb_size'])) { 221 $this->setThumbSize((int) $data['thumb_size']); 222 } 223 224 if ($this->isValidOptionLabel($data['opt_label'])) { 225 $this->setOptionLabel($data['opt_label']); 226 } 227 228 $this->setCustomTrueOptionLabel($data['custom_true']); 229 $this->setCustomFalseOptionLabel($data['custom_false']); 230 231 if ($data['score_partsol'] !== null) { 232 $this->setScorePartialSolutionEnabled((bool) $data['score_partsol']); 233 } 234 235 if (isset($data['feedback_setting'])) { 236 $this->setSpecificFeedbackSetting((int) $data['feedback_setting']); 237 } 238 239 try { 240 $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); 241 } catch (ilTestQuestionPoolInvalidArgumentException $e) { 242 $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); 243 } 244 245 try { 246 $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']); 247 } catch (ilTestQuestionPoolException $e) { 248 } 249 } 250 251 $this->loadAnswerData($questionId); 252 253 parent::loadFromDb($questionId); 254 } 255 256 private function loadAnswerData($questionId) 257 { 258 global $DIC; 259 $ilDB = $DIC['ilDB']; 260 261 $res = $this->db->queryF( 262 "SELECT * FROM {$this->getAnswerTableName()} WHERE question_fi = %s ORDER BY position ASC", 263 array('integer'), 264 array($questionId) 265 ); 266 267 require_once 'Modules/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php'; 268 require_once 'Services/RTE/classes/class.ilRTE.php'; 269 270 while ($data = $ilDB->fetchAssoc($res)) { 271 $answer = new ilAssKprimChoiceAnswer(); 272 273 $answer->setPosition($data['position']); 274 275 $answer->setAnswertext(ilRTE::_replaceMediaObjectImageSrc($data['answertext'], 1)); 276 277 $answer->setImageFile($data['imagefile']); 278 $answer->setThumbPrefix($this->getThumbPrefix()); 279 $answer->setImageFsDir($this->getImagePath()); 280 $answer->setImageWebDir($this->getImagePathWeb()); 281 282 $answer->setCorrectness($data['correctness']); 283 284 $this->answers[$answer->getPosition()] = $answer; 285 } 286 287 for ($i = count($this->answers); $i < self::NUM_REQUIRED_ANSWERS; $i++) { 288 $answer = new ilAssKprimChoiceAnswer(); 289 290 $answer->setPosition($i); 291 292 $this->answers[$answer->getPosition()] = $answer; 293 } 294 } 295 296 public function saveToDb($originalId = '') 297 { 298 $this->saveQuestionDataToDb($originalId); 299 300 $this->saveAdditionalQuestionDataToDb(); 301 $this->saveAnswerSpecificDataToDb(); 302 303 parent::saveToDb($originalId); 304 } 305 306 public function saveAdditionalQuestionDataToDb() 307 { 308 $this->db->replace( 309 $this->getAdditionalTableName(), 310 array( 311 'question_fi' => array('integer', $this->getId()) 312 ), 313 array( 314 'shuffle_answers' => array('integer', (int) $this->isShuffleAnswersEnabled()), 315 'answer_type' => array('text', $this->getAnswerType()), 316 'thumb_size' => array('integer', (int) $this->getThumbSize()), 317 'opt_label' => array('text', $this->getOptionLabel()), 318 'custom_true' => array('text', $this->getCustomTrueOptionLabel()), 319 'custom_false' => array('text', $this->getCustomFalseOptionLabel()), 320 'score_partsol' => array('integer', (int) $this->isScorePartialSolutionEnabled()), 321 'feedback_setting' => array('integer', (int) $this->getSpecificFeedbackSetting()) 322 ) 323 ); 324 } 325 326 public function saveAnswerSpecificDataToDb() 327 { 328 foreach ($this->getAnswers() as $answer) { 329 $this->db->replace( 330 $this->getAnswerTableName(), 331 array( 332 'question_fi' => array('integer', (int) $this->getId()), 333 'position' => array('integer', (int) $answer->getPosition()) 334 ), 335 array( 336 'answertext' => array('text', $answer->getAnswertext()), 337 'imagefile' => array('text', $answer->getImageFile()), 338 'correctness' => array('integer', (int) $answer->getCorrectness()) 339 ) 340 ); 341 } 342 343 $this->rebuildThumbnails(); 344 } 345 346 public function isComplete() 347 { 348 foreach (array($this->title, $this->author, $this->question) as $text) { 349 if (!strlen($text)) { 350 return false; 351 } 352 } 353 354 if ($this->getMaximumPoints() <= 0) { 355 return false; 356 } 357 358 foreach ($this->getAnswers() as $answer) { 359 /* @var ilAssKprimChoiceAnswer $answer */ 360 361 if (is_null($answer->getCorrectness())) { 362 return false; 363 } 364 365 if (!strlen($answer->getAnswertext()) && !strlen($answer->getImageFile())) { 366 return false; 367 } 368 } 369 370 return true; 371 } 372 373 /** 374 * Saves the learners input of the question to the database. 375 * 376 * @access public 377 * @param integer $active_id Active id of the user 378 * @param integer $pass Test pass 379 * @return boolean $status 380 */ 381 public function saveWorkingData($active_id, $pass = null, $authorized = true) 382 { 383 /** @var ilDBInterface $ilDB */ 384 $ilDB = $GLOBALS['DIC']['ilDB']; 385 386 if (is_null($pass)) { 387 include_once "./Modules/Test/classes/class.ilObjTest.php"; 388 $pass = ilObjTest::_getPass($active_id); 389 } 390 391 $entered_values = 0; 392 393 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use (&$entered_values, $active_id, $pass, $authorized) { 394 $this->removeCurrentSolution($active_id, $pass, $authorized); 395 396 $solutionSubmit = $this->getSolutionSubmit(); 397 398 foreach ($solutionSubmit as $answerIndex => $answerValue) { 399 $this->saveCurrentSolution($active_id, $pass, (int) $answerIndex, (int) $answerValue, $authorized); 400 $entered_values++; 401 } 402 }); 403 404 if ($entered_values) { 405 include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php"); 406 if (ilObjAssessmentFolder::_enabledAssessmentLogging()) { 407 assQuestion::logAction($this->lng->txtlng("assessment", "log_user_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId()); 408 } 409 } else { 410 include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php"); 411 if (ilObjAssessmentFolder::_enabledAssessmentLogging()) { 412 assQuestion::logAction($this->lng->txtlng("assessment", "log_user_not_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId()); 413 } 414 } 415 416 return true; 417 } 418 419 /** 420 * Returns the points, a learner has reached answering the question. 421 * The points are calculated from the given answers. 422 * 423 * @access public 424 * @param integer $active_id 425 * @param integer $pass 426 * @param boolean $returndetails (deprecated !!) 427 * @return integer/array $points/$details (array $details is deprecated !!) 428 */ 429 public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false) 430 { 431 if ($returndetails) { 432 throw new ilTestException('return details not implemented for ' . __METHOD__); 433 } 434 435 global $DIC; 436 $ilDB = $DIC['ilDB']; 437 438 $found_values = array(); 439 if (is_null($pass)) { 440 $pass = $this->getSolutionMaxPass($active_id); 441 } 442 443 $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution); 444 445 while ($data = $ilDB->fetchAssoc($result)) { 446 $found_values[(int) $data['value1']] = (int) $data['value2']; 447 } 448 449 $points = $this->calculateReachedPointsForSolution($found_values, $active_id); 450 451 return $points; 452 } 453 454 public function getValidAnswerTypes() 455 { 456 return array(self::ANSWER_TYPE_SINGLE_LINE, self::ANSWER_TYPE_MULTI_LINE); 457 } 458 459 public function isValidAnswerType($answerType) 460 { 461 $validTypes = $this->getValidAnswerTypes(); 462 return in_array($answerType, $validTypes); 463 } 464 465 public function isSingleLineAnswerType($answerType) 466 { 467 return $answerType == assKprimChoice::ANSWER_TYPE_SINGLE_LINE; 468 } 469 470 /** 471 * @param ilLanguage $lng 472 * @return array 473 */ 474 public function getAnswerTypeSelectOptions(ilLanguage $lng) 475 { 476 return array( 477 self::ANSWER_TYPE_SINGLE_LINE => $lng->txt('answers_singleline'), 478 self::ANSWER_TYPE_MULTI_LINE => $lng->txt('answers_multiline') 479 ); 480 } 481 482 public function getValidOptionLabels() 483 { 484 return array( 485 self::OPTION_LABEL_RIGHT_WRONG, 486 self::OPTION_LABEL_PLUS_MINUS, 487 self::OPTION_LABEL_APPLICABLE_OR_NOT, 488 self::OPTION_LABEL_ADEQUATE_OR_NOT, 489 self::OPTION_LABEL_CUSTOM 490 ); 491 } 492 493 public function getValidOptionLabelsTranslated(ilLanguage $lng) 494 { 495 return array( 496 self::OPTION_LABEL_RIGHT_WRONG => $lng->txt('option_label_right_wrong'), 497 self::OPTION_LABEL_PLUS_MINUS => $lng->txt('option_label_plus_minus'), 498 self::OPTION_LABEL_APPLICABLE_OR_NOT => $lng->txt('option_label_applicable_or_not'), 499 self::OPTION_LABEL_ADEQUATE_OR_NOT => $lng->txt('option_label_adequate_or_not'), 500 self::OPTION_LABEL_CUSTOM => $lng->txt('option_label_custom') 501 ); 502 } 503 504 public function isValidOptionLabel($optionLabel) 505 { 506 $validLabels = $this->getValidOptionLabels(); 507 return in_array($optionLabel, $validLabels); 508 } 509 510 public function getTrueOptionLabelTranslation(ilLanguage $lng, $optionLabel) 511 { 512 switch ($optionLabel) { 513 case self::OPTION_LABEL_RIGHT_WRONG: 514 return $lng->txt('option_label_right'); 515 516 case self::OPTION_LABEL_PLUS_MINUS: 517 return $lng->txt('option_label_plus'); 518 519 case self::OPTION_LABEL_APPLICABLE_OR_NOT: 520 return $lng->txt('option_label_applicable'); 521 522 case self::OPTION_LABEL_ADEQUATE_OR_NOT: 523 return $lng->txt('option_label_adequate'); 524 525 case self::OPTION_LABEL_CUSTOM: 526 return $this->getCustomTrueOptionLabel(); 527 } 528 } 529 530 public function getFalseOptionLabelTranslation(ilLanguage $lng, $optionLabel) 531 { 532 switch ($optionLabel) { 533 case self::OPTION_LABEL_RIGHT_WRONG: 534 return $lng->txt('option_label_wrong'); 535 536 case self::OPTION_LABEL_PLUS_MINUS: 537 return $lng->txt('option_label_minus'); 538 539 case self::OPTION_LABEL_APPLICABLE_OR_NOT: 540 return $lng->txt('option_label_not_applicable'); 541 542 case self::OPTION_LABEL_ADEQUATE_OR_NOT: 543 return $lng->txt('option_label_not_adequate'); 544 545 case self::OPTION_LABEL_CUSTOM: 546 return $this->getCustomFalseOptionLabel(); 547 } 548 } 549 550 public function getInstructionTextTranslation(ilLanguage $lng, $optionLabel) 551 { 552 return sprintf( 553 $lng->txt('kprim_instruction_text'), 554 $this->getTrueOptionLabelTranslation($lng, $optionLabel), 555 $this->getFalseOptionLabelTranslation($lng, $optionLabel) 556 ); 557 } 558 559 public function isCustomOptionLabel($labelValue) 560 { 561 return $labelValue == self::OPTION_LABEL_CUSTOM; 562 } 563 564 public function getThumbPrefix() 565 { 566 return self::THUMB_PREFIX; 567 } 568 569 public function rebuildThumbnails() 570 { 571 if ($this->isSingleLineAnswerType($this->getAnswerType()) && $this->getThumbSize()) { 572 foreach ($this->getAnswers() as $answer) { 573 if (strlen($answer->getImageFile())) { 574 $this->generateThumbForFile($answer->getImageFsDir(), $answer->getImageFile()); 575 } 576 } 577 } 578 } 579 580 protected function generateThumbForFile($path, $file) 581 { 582 $filename = $path . $file; 583 if (@file_exists($filename)) { 584 $thumbpath = $path . $this->getThumbPrefix() . $file; 585 $path_info = @pathinfo($filename); 586 $ext = ""; 587 switch (strtoupper($path_info['extension'])) { 588 case 'PNG': 589 $ext = 'PNG'; 590 break; 591 case 'GIF': 592 $ext = 'GIF'; 593 break; 594 default: 595 $ext = 'JPEG'; 596 break; 597 } 598 ilUtil::convertImage($filename, $thumbpath, $ext, $this->getThumbSize()); 599 } 600 } 601 602 public function handleFileUploads($answers, $files) 603 { 604 foreach ($answers as $answer) { 605 /* @var ilAssKprimChoiceAnswer $answer */ 606 607 if (!isset($files[$answer->getPosition()])) { 608 continue; 609 } 610 611 $this->handleFileUpload($answer, $files[$answer->getPosition()]); 612 } 613 } 614 615 private function handleFileUpload(ilAssKprimChoiceAnswer $answer, $fileData) 616 { 617 $imagePath = $this->getImagePath(); 618 619 if (!file_exists($imagePath)) { 620 ilUtil::makeDirParents($imagePath); 621 } 622 623 $filename = $this->buildHashedImageFilename($fileData['name'], true); 624 625 $answer->setImageFsDir($imagePath); 626 $answer->setImageFile($filename); 627 628 if (!ilUtil::moveUploadedFile($fileData['tmp_name'], $fileData['name'], $answer->getImageFsPath())) { 629 return 2; 630 } 631 632 return 0; 633 } 634 635 public function removeAnswerImage($position) 636 { 637 $answer = $this->getAnswer($position); 638 639 if (file_exists($answer->getImageFsPath())) { 640 ilUtil::delDir($answer->getImageFsPath()); 641 } 642 643 if (file_exists($answer->getThumbFsPath())) { 644 ilUtil::delDir($answer->getThumbFsPath()); 645 } 646 647 $answer->setImageFile(null); 648 } 649 650 protected function getSolutionSubmit() 651 { 652 $solutionSubmit = array(); 653 foreach ($_POST as $key => $value) { 654 $matches = null; 655 656 if (preg_match("/^kprim_choice_result_(\d+)/", $key, $matches)) { 657 if (strlen($value)) { 658 $solutionSubmit[$matches[1]] = $value; 659 } 660 } 661 } 662 return $solutionSubmit; 663 } 664 665 protected function calculateReachedPointsForSolution($found_values, $active_id = 0) 666 { 667 $numCorrect = 0; 668 669 foreach ($this->getAnswers() as $key => $answer) { 670 if (!isset($found_values[$answer->getPosition()])) { 671 continue; 672 } 673 674 if ($found_values[$answer->getPosition()] == $answer->getCorrectness()) { 675 $numCorrect++; 676 } 677 } 678 679 if ($numCorrect >= self::NUM_REQUIRED_ANSWERS) { 680 $points = $this->getPoints(); 681 } elseif ($this->isScorePartialSolutionEnabled() && $numCorrect >= self::PARTIAL_SCORING_NUM_CORRECT_ANSWERS) { 682 $points = $this->getPoints() / 2; 683 } else { 684 $points = 0; 685 } 686 687 if ($active_id) { 688 include_once "./Modules/Test/classes/class.ilObjTest.php"; 689 $mc_scoring = ilObjTest::_getMCScoring($active_id); 690 if (($mc_scoring == 0) && (count($found_values) == 0)) { 691 $points = 0; 692 } 693 } 694 return $points; 695 } 696 697 public function duplicate($for_test = true, $title = "", $author = "", $owner = "", $testObjId = null) 698 { 699 if ($this->id <= 0) { 700 // The question has not been saved. It cannot be duplicated 701 return; 702 } 703 // duplicate the question in database 704 $this_id = $this->getId(); 705 $thisObjId = $this->getObjId(); 706 707 $clone = $this; 708 include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php"); 709 $original_id = assQuestion::_getOriginalId($this->id); 710 $clone->id = -1; 711 712 if ((int) $testObjId > 0) { 713 $clone->setObjId($testObjId); 714 } 715 716 if ($title) { 717 $clone->setTitle($title); 718 } 719 720 if ($author) { 721 $clone->setAuthor($author); 722 } 723 if ($owner) { 724 $clone->setOwner($owner); 725 } 726 727 if ($for_test) { 728 $clone->saveToDb($original_id); 729 } else { 730 $clone->saveToDb(); 731 } 732 733 // copy question page content 734 $clone->copyPageOfQuestion($this_id); 735 // copy XHTML media objects 736 $clone->copyXHTMLMediaObjectsOfQuestion($this_id); 737 // duplicate the images 738 $clone->cloneAnswerImages($this_id, $thisObjId, $clone->getId(), $clone->getObjId()); 739 740 $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId()); 741 742 return $clone->id; 743 } 744 745 public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = "") 746 { 747 if ($this->id <= 0) { 748 // The question has not been saved. It cannot be duplicated 749 return; 750 } 751 752 include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php"); 753 754 $sourceQuestionId = $this->id; 755 $sourceParentId = $this->getObjId(); 756 757 // duplicate the question in database 758 $clone = $this; 759 $clone->id = -1; 760 761 $clone->setObjId($targetParentId); 762 763 if ($targetQuestionTitle) { 764 $clone->setTitle($targetQuestionTitle); 765 } 766 767 $clone->saveToDb(); 768 // copy question page content 769 $clone->copyPageOfQuestion($sourceQuestionId); 770 // copy XHTML media objects 771 $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId); 772 // duplicate the image 773 $clone->cloneAnswerImages($sourceQuestionId, $sourceParentId, $clone->getId(), $clone->getObjId()); 774 775 $clone->onCopy($sourceParentId, $sourceQuestionId, $targetParentId, $clone->getId()); 776 777 return $clone->id; 778 } 779 780 /** 781 * Copies an assMultipleChoice object 782 */ 783 public function copyObject($target_questionpool_id, $title = "") 784 { 785 if ($this->id <= 0) { 786 // The question has not been saved. It cannot be duplicated 787 return; 788 } 789 // duplicate the question in database 790 $clone = $this; 791 include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php"); 792 $original_id = assQuestion::_getOriginalId($this->id); 793 $clone->id = -1; 794 $source_questionpool_id = $this->getObjId(); 795 $clone->setObjId($target_questionpool_id); 796 if ($title) { 797 $clone->setTitle($title); 798 } 799 $clone->saveToDb(); 800 // copy question page content 801 $clone->copyPageOfQuestion($original_id); 802 // copy XHTML media objects 803 $clone->copyXHTMLMediaObjectsOfQuestion($original_id); 804 // duplicate the image 805 $clone->cloneAnswerImages($original_id, $source_questionpool_id, $clone->getId(), $clone->getObjId()); 806 807 $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId()); 808 809 return $clone->id; 810 } 811 812 protected function beforeSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId) 813 { 814 parent::beforeSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId); 815 816 $question = self::_instanciateQuestion($origQuestionId); 817 818 foreach ($question->getAnswers() as $answer) { 819 $question->removeAnswerImage($answer->getPosition()); 820 } 821 } 822 823 protected function afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId) 824 { 825 parent::afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId); 826 827 $this->cloneAnswerImages($dupQuestionId, $dupParentObjId, $origQuestionId, $origParentObjId); 828 } 829 830 protected function cloneAnswerImages($sourceQuestionId, $sourceParentId, $targetQuestionId, $targetParentId) 831 { 832 /** @var $ilLog ilLogger */ 833 global $DIC; 834 $ilLog = $DIC['ilLog']; 835 836 $sourcePath = $this->buildImagePath($sourceQuestionId, $sourceParentId); 837 $targetPath = $this->buildImagePath($targetQuestionId, $targetParentId); 838 839 foreach ($this->getAnswers() as $answer) { 840 $filename = $answer->getImageFile(); 841 842 if (strlen($filename)) { 843 if (!file_exists($targetPath)) { 844 ilUtil::makeDirParents($targetPath); 845 } 846 847 if (file_exists($sourcePath . $filename)) { 848 if (!copy($sourcePath . $filename, $targetPath . $filename)) { 849 $ilLog->warning(sprintf( 850 "Could not clone source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)", 851 $sourcePath . $filename, 852 $targetPath . $filename, 853 $sourceQuestionId, 854 $targetQuestionId, 855 $sourceParentId, 856 $targetParentId 857 )); 858 } 859 } 860 861 if (file_exists($sourcePath . $this->getThumbPrefix() . $filename)) { 862 if (!copy($sourcePath . $this->getThumbPrefix() . $filename, $targetPath . $this->getThumbPrefix() . $filename)) { 863 $ilLog->warning(sprintf( 864 "Could not clone thumbnail source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)", 865 $sourcePath . $this->getThumbPrefix() . $filename, 866 $targetPath . $this->getThumbPrefix() . $filename, 867 $sourceQuestionId, 868 $targetQuestionId, 869 $sourceParentId, 870 $targetParentId 871 )); 872 } 873 } 874 } 875 } 876 } 877 878 protected function getRTETextWithMediaObjects() 879 { 880 $combinedText = parent::getRTETextWithMediaObjects(); 881 882 foreach ($this->getAnswers() as $answer) { 883 $combinedText .= $answer->getAnswertext(); 884 } 885 886 return $combinedText; 887 } 888 889 /** 890 * @param ilAssSelfAssessmentMigrator $migrator 891 */ 892 protected function lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator) 893 { 894 foreach ($this->getAnswers() as $answer) { 895 /* @var ilAssKprimChoiceAnswer $answer */ 896 $answer->setAnswertext($migrator->migrateToLmContent($answer->getAnswertext())); 897 } 898 } 899 900 /** 901 * Returns a JSON representation of the question 902 */ 903 public function toJSON() 904 { 905 $this->lng->loadLanguageModule('assessment'); 906 907 require_once './Services/RTE/classes/class.ilRTE.php'; 908 $result = array(); 909 $result['id'] = (int) $this->getId(); 910 $result['type'] = (string) $this->getQuestionType(); 911 $result['title'] = (string) $this->getTitle(); 912 $result['question'] = $this->formatSAQuestion($this->getQuestion()); 913 $result['instruction'] = $this->getInstructionTextTranslation( 914 $this->lng, 915 $this->getOptionLabel() 916 ); 917 $result['nr_of_tries'] = (int) $this->getNrOfTries(); 918 $result['shuffle'] = (bool) $this->isShuffleAnswersEnabled(); 919 $result['feedback'] = array( 920 'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)), 921 'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true)) 922 ); 923 924 $result['trueOptionLabel'] = $this->getTrueOptionLabelTranslation($this->lng, $this->getOptionLabel()); 925 $result['falseOptionLabel'] = $this->getFalseOptionLabelTranslation($this->lng, $this->getOptionLabel()); 926 927 $result['num_allowed_failures'] = $this->getNumAllowedFailures(); 928 929 $answers = array(); 930 $has_image = false; 931 932 foreach ($this->getAnswers() as $key => $answer) { 933 if (strlen((string) $answer->getImageFile())) { 934 $has_image = true; 935 } 936 937 $answers[] = array( 938 'answertext' => (string) $this->formatSAQuestion($answer->getAnswertext()), 939 'correctness' => (bool) $answer->getCorrectness(), 940 'order' => (int) $answer->getPosition(), 941 'image' => (string) $answer->getImageFile(), 942 'feedback' => $this->formatSAQuestion( 943 $this->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation($this->getId(), 0, $key) 944 ) 945 ); 946 } 947 948 $result['answers'] = $answers; 949 950 if ($has_image) { 951 $result['path'] = $this->getImagePathWeb(); 952 $result['thumb'] = $this->getThumbSize(); 953 } 954 955 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId()); 956 $result['mobs'] = $mobs; 957 958 return json_encode($result); 959 } 960 961 private function getNumAllowedFailures() 962 { 963 if ($this->isScorePartialSolutionEnabled()) { 964 return self::NUM_REQUIRED_ANSWERS - self::PARTIAL_SCORING_NUM_CORRECT_ANSWERS; 965 } 966 967 return 0; 968 } 969 970 public function getSpecificFeedbackAllCorrectOptionLabel() 971 { 972 return 'feedback_correct_kprim'; 973 } 974 975 public static function isObligationPossible($questionId) 976 { 977 return true; 978 } 979 980 public function isAnswered($active_id, $pass = null) 981 { 982 $numExistingSolutionRecords = assQuestion::getNumExistingSolutionRecords($active_id, $pass, $this->getId()); 983 984 return $numExistingSolutionRecords >= 4; 985 } 986 987 /** 988 * {@inheritdoc} 989 */ 990 public function setExportDetailsXLS($worksheet, $startrow, $active_id, $pass) 991 { 992 parent::setExportDetailsXLS($worksheet, $startrow, $active_id, $pass); 993 994 $solution = $this->getSolutionValues($active_id, $pass); 995 996 $i = 1; 997 foreach ($this->getAnswers() as $id => $answer) { 998 $worksheet->setCell($startrow + $i, 0, $answer->getAnswertext()); 999 $worksheet->setBold($worksheet->getColumnCoord(0) . ($startrow + $i)); 1000 $correctness = false; 1001 foreach ($solution as $solutionvalue) { 1002 if ($id == $solutionvalue['value1']) { 1003 $correctness = $solutionvalue['value2']; 1004 break; 1005 } 1006 } 1007 $worksheet->setCell($startrow + $i, 1, $correctness); 1008 $i++; 1009 } 1010 1011 return $startrow + $i + 1; 1012 } 1013 1014 public function moveAnswerDown($position) 1015 { 1016 if ($position < 0 || $position >= (self::NUM_REQUIRED_ANSWERS - 1)) { 1017 return false; 1018 } 1019 1020 for ($i = 0, $max = count($this->answers); $i < $max; $i++) { 1021 if ($i == $position) { 1022 $movingAnswer = $this->answers[$i]; 1023 $targetAnswer = $this->answers[ $i + 1 ]; 1024 1025 $movingAnswer->setPosition($position + 1); 1026 $targetAnswer->setPosition($position); 1027 1028 $this->answers[ $i + 1 ] = $movingAnswer; 1029 $this->answers[$i] = $targetAnswer; 1030 } 1031 } 1032 } 1033 1034 public function moveAnswerUp($position) 1035 { 1036 if ($position <= 0 || $position > (self::NUM_REQUIRED_ANSWERS - 1)) { 1037 return false; 1038 } 1039 1040 for ($i = 0, $max = count($this->answers); $i < $max; $i++) { 1041 if ($i == $position) { 1042 $movingAnswer = $this->answers[$i]; 1043 $targetAnswer = $this->answers[ $i - 1 ]; 1044 1045 $movingAnswer->setPosition($position - 1); 1046 $targetAnswer->setPosition($position); 1047 1048 $this->answers[ $i - 1 ] = $movingAnswer; 1049 $this->answers[$i] = $targetAnswer; 1050 } 1051 } 1052 1053 return true; 1054 } 1055} 1056