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/Test/classes/inc.AssessmentConstants.php'; 6require_once './Modules/TestQuestionPool/interfaces/interface.ilObjQuestionScoringAdjustable.php'; 7require_once './Modules/TestQuestionPool/interfaces/interface.ilObjAnswerScoringAdjustable.php'; 8require_once './Modules/TestQuestionPool/interfaces/interface.iQuestionCondition.php'; 9require_once './Modules/TestQuestionPool/classes/class.ilUserQuestionResult.php'; 10 11require_once 'Modules/TestQuestionPool/classes/questions/class.ilAssOrderingElementList.php'; 12 13/** 14 * Class for ordering questions 15 * 16 * assOrderingQuestion is a class for ordering questions. 17 * 18 * @author Helmut Schottmüller <helmut.schottmueller@mac.com> 19 * @author Björn Heyser <bheyser@databay.de> 20 * @author Maximilian Becker <mbecker@databay.de> 21 * 22 * @version $Id$ 23 * 24 * @ingroup ModulesTestQuestionPool 25 */ 26class assOrderingQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition 27{ 28 const ORDERING_ELEMENT_FORM_FIELD_POSTVAR = 'order_elems'; 29 30 const ORDERING_ELEMENT_FORM_CMD_UPLOAD_IMG = 'uploadElementImage'; 31 const ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG = 'removeElementImage'; 32 33 /** 34 * @var ilAssOrderingElementList 35 */ 36 protected $orderingElementList; 37 38 /** 39 * Type of ordering question 40 * 41 * There are two possible types of ordering questions: Ordering terms (=1) 42 * and Ordering pictures (=0). 43 * 44 * @var integer 45 */ 46 public $ordering_type; 47 48 /** 49 * Maximum thumbnail geometry 50 * 51 * @var integer 52 */ 53 public $thumb_geometry = 100; 54 55 /** 56 * Minimum element height 57 * 58 * @var integer 59 */ 60 public $element_height; 61 62 public $old_ordering_depth = array(); 63 public $leveled_ordering = array(); 64 65 /** 66 * assOrderingQuestion constructor 67 * 68 * The constructor takes possible arguments an creates an instance of the assOrderingQuestion object. 69 * 70 * @param string $title A title string to describe the question 71 * @param string $comment A comment string to describe the question 72 * @param string $author A string containing the name of the questions author 73 * @param integer $owner A numerical ID to identify the owner/creator 74 * @param string $question The question string of the ordering test 75 * @param int $ordering_type 76 */ 77 public function __construct( 78 $title = "", 79 $comment = "", 80 $author = "", 81 $owner = -1, 82 $question = "", 83 $ordering_type = OQ_TERMS 84 ) { 85 parent::__construct($title, $comment, $author, $owner, $question); 86 $this->orderingElementList = new ilAssOrderingElementList(); 87 $this->ordering_type = $ordering_type; 88 } 89 90 /** 91 * Returns true, if a ordering question is complete for use 92 * 93 * @return boolean True, if the ordering question is complete for use, otherwise false 94 */ 95 public function isComplete() 96 { 97 if (!$this->getAuthor()) { 98 return false; 99 } 100 101 if (!$this->getTitle()) { 102 return false; 103 } 104 105 if (!$this->getQuestion()) { 106 return false; 107 } 108 109 if (!$this->getMaximumPoints()) { 110 return false; 111 } 112 113 if (!$this->getOrderingElementList()->countElements()) { 114 return false; 115 } 116 117 return true; 118 } 119 120 /** 121 * Saves a assOrderingQuestion object to a database 122 * 123 * @param string $original_id 124 * 125 * @internal param object $db A pear DB object 126 */ 127 public function saveToDb($original_id = "") 128 { 129 global $DIC; 130 $ilDB = $DIC['ilDB']; 131 132 $this->saveQuestionDataToDb($original_id); 133 $this->saveAdditionalQuestionDataToDb(); 134 $this->saveAnswerSpecificDataToDb(); 135 parent::saveToDb($original_id); 136 } 137 138 /** 139 * Loads a assOrderingQuestion object from a database 140 * 141 * @param object $db A pear DB object 142 * @param integer $question_id A unique key which defines the multiple choice test in the database 143 * @access public 144 */ 145 public function loadFromDb($question_id) 146 { 147 global $DIC; 148 $ilDB = $DIC['ilDB']; 149 150 $result = $ilDB->queryF( 151 "SELECT qpl_questions.*, " . $this->getAdditionalTableName() . ".* FROM qpl_questions LEFT JOIN " . $this->getAdditionalTableName() . " ON " . $this->getAdditionalTableName() . ".question_fi = qpl_questions.question_id WHERE qpl_questions.question_id = %s", 152 array("integer"), 153 array($question_id) 154 ); 155 if ($result->numRows() == 1) { 156 $data = $ilDB->fetchAssoc($result); 157 $this->setId($question_id); 158 $this->setObjId($data["obj_fi"]); 159 $this->setTitle($data["title"]); 160 $this->setComment($data["description"]); 161 $this->setOriginalId($data["original_id"]); 162 $this->setAuthor($data["author"]); 163 $this->setNrOfTries($data['nr_of_tries']); 164 $this->setPoints($data["points"]); 165 $this->setOwner($data["owner"]); 166 include_once("./Services/RTE/classes/class.ilRTE.php"); 167 $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($data["question_text"], 1)); 168 $this->ordering_type = strlen($data["ordering_type"]) ? $data["ordering_type"] : OQ_TERMS; 169 $this->thumb_geometry = $data["thumb_geometry"]; 170 $this->element_height = $data["element_height"]; 171 $this->setEstimatedWorkingTime(substr($data["working_time"], 0, 2), substr($data["working_time"], 3, 2), substr($data["working_time"], 6, 2)); 172 173 try { 174 $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']); 175 } catch (ilTestQuestionPoolException $e) { 176 } 177 } 178 179 $this->orderingElementList->setQuestionId($this->getId()); 180 $this->orderingElementList->loadFromDb(); 181 182 parent::loadFromDb($question_id); 183 } 184 185 /** 186 * Duplicates an assOrderingQuestion 187 * 188 * @access public 189 */ 190 public function duplicate($for_test = true, $title = "", $author = "", $owner = "", $testObjId = null) 191 { 192 if ($this->id <= 0) { 193 // The question has not been saved. It cannot be duplicated 194 return; 195 } 196 // duplicate the question in database 197 $this_id = $this->getId(); 198 $thisObjId = $this->getObjId(); 199 200 $clone = $this; 201 include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php"); 202 $original_id = assQuestion::_getOriginalId($this->id); 203 $clone->id = -1; 204 205 if ((int) $testObjId > 0) { 206 $clone->setObjId($testObjId); 207 } 208 209 if ($title) { 210 $clone->setTitle($title); 211 } 212 if ($author) { 213 $clone->setAuthor($author); 214 } 215 if ($owner) { 216 $clone->setOwner($owner); 217 } 218 if ($for_test) { 219 $clone->saveToDb($original_id); 220 } else { 221 $clone->saveToDb(); 222 } 223 224 $clone->duplicateOrderlingElementList(); 225 226 // copy question page content 227 $clone->copyPageOfQuestion($this_id); 228 // copy XHTML media objects 229 $clone->copyXHTMLMediaObjectsOfQuestion($this_id); 230 // duplicate the image 231 $clone->duplicateImages($this_id, $thisObjId, $clone->getId(), $testObjId); 232 233 $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId()); 234 235 return $clone->id; 236 } 237 238 protected function duplicateOrderlingElementList() 239 { 240 $this->getOrderingElementList()->setQuestionId($this->getId()); 241 $this->getOrderingElementList()->distributeNewRandomIdentifiers(); 242 $this->getOrderingElementList()->saveToDb(); 243 } 244 245 /** 246 * Copies an assOrderingQuestion object 247 * 248 * @access public 249 */ 250 public function copyObject($target_questionpool_id, $title = "") 251 { 252 if ($this->id <= 0) { 253 // The question has not been saved. It cannot be duplicated 254 return; 255 } 256 // duplicate the question in database 257 $clone = $this; 258 include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php"); 259 $original_id = assQuestion::_getOriginalId($this->id); 260 $clone->id = -1; 261 $source_questionpool_id = $this->getObjId(); 262 $clone->setObjId($target_questionpool_id); 263 if ($title) { 264 $clone->setTitle($title); 265 } 266 267 $clone->saveToDb(); 268 269 // copy question page content 270 $clone->copyPageOfQuestion($original_id); 271 // copy XHTML media objects 272 $clone->copyXHTMLMediaObjectsOfQuestion($original_id); 273 // duplicate the image 274 $clone->duplicateImages($original_id, $source_questionpool_id, $clone->getId(), $target_questionpool_id); 275 276 $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId()); 277 278 return $clone->id; 279 } 280 281 public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = "") 282 { 283 if ($this->id <= 0) { 284 // The question has not been saved. It cannot be duplicated 285 return; 286 } 287 288 include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php"); 289 290 $sourceQuestionId = $this->id; 291 $sourceParentId = $this->getObjId(); 292 293 // duplicate the question in database 294 $clone = $this; 295 $clone->id = -1; 296 297 $clone->setObjId($targetParentId); 298 299 if ($targetQuestionTitle) { 300 $clone->setTitle($targetQuestionTitle); 301 } 302 303 $clone->saveToDb(); 304 // copy question page content 305 $clone->copyPageOfQuestion($sourceQuestionId); 306 // copy XHTML media objects 307 $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId); 308 // duplicate the image 309 $clone->duplicateImages($sourceQuestionId, $sourceParentId, $clone->getId(), $clone->getObjId()); 310 311 $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId()); 312 313 return $clone->id; 314 } 315 316 public function duplicateImages($src_question_id, $src_object_id, $dest_question_id, $dest_object_id) 317 { 318 global $DIC; 319 $ilLog = $DIC['ilLog']; 320 if ($this->getOrderingType() == OQ_PICTURES || $this->getOrderingType() == OQ_NESTED_PICTURES) { 321 $imagepath_original = $this->getImagePath($src_question_id, $src_object_id); 322 $imagepath = $this->getImagePath($dest_question_id, $dest_object_id); 323 324 if (!file_exists($imagepath)) { 325 ilUtil::makeDirParents($imagepath); 326 } 327 foreach ($this->getOrderingElementList() as $element) { 328 $filename = $element->getContent(); 329 if (!@copy($imagepath_original . $filename, $imagepath . $filename)) { 330 $ilLog->write("image could not be duplicated!!!!"); 331 } 332 if (@file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) { 333 if (!@copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) { 334 $ilLog->write("image thumbnail could not be duplicated!!!!"); 335 } 336 } 337 } 338 } 339 } 340 341 /** 342 * @deprecated (!) 343 * simply use the working method duplicateImages(), we do not search the difference here 344 * and we will delete this soon (!) currently no usage found, remove for il5.3 345 */ 346 public function copyImages($question_id, $source_questionpool) 347 { 348 global $DIC; 349 $ilLog = $DIC['ilLog']; 350 if ($this->getOrderingType() == OQ_PICTURES) { 351 $imagepath = $this->getImagePath(); 352 $imagepath_original = str_replace("/$this->id/images", "/$question_id/images", $imagepath); 353 $imagepath_original = str_replace("/$this->obj_id/", "/$source_questionpool/", $imagepath_original); 354 if (!file_exists($imagepath)) { 355 ilUtil::makeDirParents($imagepath); 356 } 357 foreach ($this->getOrderingElementList() as $element) { 358 $filename = $element->getContent(); 359 if (!@copy($imagepath_original . $filename, $imagepath . $filename)) { 360 $ilLog->write("Ordering Question image could not be copied: ${imagepath_original}${filename}"); 361 } 362 if (@file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) { 363 if (!@copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) { 364 $ilLog->write("Ordering Question image thumbnail could not be copied: $imagepath_original" . $this->getThumbPrefix() . $filename); 365 } 366 } 367 } 368 } 369 } 370 371 /** 372 * Sets the ordering question type 373 * 374 * @param integer $ordering_type The question ordering type 375 * @access public 376 * @see $ordering_type 377 */ 378 public function setOrderingType($ordering_type = OQ_TERMS) 379 { 380 $this->ordering_type = $ordering_type; 381 } 382 383 /** 384 * Returns the ordering question type 385 * 386 * @return integer The ordering question type 387 * @access public 388 * @see $ordering_type 389 */ 390 public function getOrderingType() 391 { 392 return $this->ordering_type; 393 } 394 395 public function isOrderingTypeNested() 396 { 397 return in_array($this->getOrderingType(), array(OQ_NESTED_TERMS, OQ_NESTED_PICTURES)); 398 } 399 400 public function isImageOrderingType() 401 { 402 return in_array($this->getOrderingType(), array(OQ_PICTURES, OQ_NESTED_PICTURES)); 403 } 404 405 public function hasOrderingTypeUploadSupport() 406 { 407 return $this->getOrderingType() == OQ_PICTURES; 408 } 409 410 /** 411 * @param $forceCorrectSolution 412 * @param $activeId 413 * @param $passIndex 414 * @return ilAssOrderingElementList 415 */ 416 public function getOrderingElementListForSolutionOutput($forceCorrectSolution, $activeId, $passIndex, $getUseIntermediateSolution = false) 417 { 418 if ($forceCorrectSolution || !$activeId || $passIndex === null) { 419 return $this->getOrderingElementList(); 420 } 421 422 $solutionValues = $this->getSolutionValues($activeId, $passIndex, !$getUseIntermediateSolution); 423 424 if (!count($solutionValues)) { 425 return $this->getShuffledOrderingElementList(); 426 } 427 428 return $this->getSolutionOrderingElementList($this->fetchIndexedValuesFromValuePairs($solutionValues)); 429 } 430 431 /** 432 * @param ilAssNestedOrderingElementsInputGUI $inputGUI 433 * @param array $lastPost 434 * @param integer $activeId 435 * @param integer $pass 436 * @return ilAssOrderingElementList 437 * @throws ilTestException 438 * @throws ilTestQuestionPoolException 439 */ 440 public function getSolutionOrderingElementListForTestOutput(ilAssNestedOrderingElementsInputGUI $inputGUI, $lastPost, $activeId, $pass) 441 { 442 if ($inputGUI->isPostSubmit($lastPost)) { 443 return $this->fetchSolutionListFromFormSubmissionData($lastPost); 444 } 445 446 // hey: prevPassSolutions - pass will be always available from now on 447 #if( $pass === null && !ilObjTest::_getUsePreviousAnswers($activeId, true) ) 448 #// condition looks strange? yes - keep it null when previous solutions not enabled (!) 449 #{ 450 # $pass = ilObjTest::_getPass($activeId); 451 #} 452 // hey. 453 454 $indexedSolutionValues = $this->fetchIndexedValuesFromValuePairs( 455 // hey: prevPassSolutions - obsolete due to central check 456 $this->getTestOutputSolutions($activeId, $pass) 457 // hey. 458 ); 459 460 if (count($indexedSolutionValues)) { 461 return $this->getSolutionOrderingElementList($indexedSolutionValues); 462 } 463 464 return $this->getShuffledOrderingElementList(); 465 } 466 467 /** 468 * @param string $value1 469 * @param string $value2 470 * @return ilAssOrderingElement 471 */ 472 protected function getSolutionValuePairBrandedOrderingElementByRandomIdentifier($value1, $value2) 473 { 474 $value2 = explode(':', $value2); 475 476 $randomIdentifier = $value2[0]; 477 $selectedPosition = $value1; 478 $selectedIndentation = $value2[1]; 479 480 $element = $this->getOrderingElementList()->getElementByRandomIdentifier($randomIdentifier)->getClone(); 481 482 $element->setPosition($selectedPosition); 483 $element->setIndentation($selectedIndentation); 484 485 return $element; 486 } 487 488 /** 489 * @param string $value1 490 * @param string $value2 491 * @return ilAssOrderingElement 492 */ 493 protected function getSolutionValuePairBrandedOrderingElementBySolutionIdentifier($value1, $value2) 494 { 495 $solutionIdentifier = $value1; 496 $selectedPosition = ($value2 - 1); 497 $selectedIndentation = 0; 498 499 $element = $this->getOrderingElementList()->getElementBySolutionIdentifier($solutionIdentifier)->getClone(); 500 501 $element->setPosition($selectedPosition); 502 $element->setIndentation($selectedIndentation); 503 504 return $element; 505 } 506 507 /** 508 * @param array $valuePairs 509 * @return ilAssOrderingElementList 510 * @throws ilTestQuestionPoolException 511 */ 512 public function getSolutionOrderingElementList($indexedSolutionValues) 513 { 514 $solutionOrderingList = new ilAssOrderingElementList(); 515 $solutionOrderingList->setQuestionId($this->getId()); 516 517 foreach ($indexedSolutionValues as $value1 => $value2) { 518 if ($this->isOrderingTypeNested()) { 519 $element = $this->getSolutionValuePairBrandedOrderingElementByRandomIdentifier($value1, $value2); 520 } else { 521 $element = $this->getSolutionValuePairBrandedOrderingElementBySolutionIdentifier($value1, $value2); 522 } 523 524 $solutionOrderingList->addElement($element); 525 } 526 527 if (!$this->getOrderingElementList()->hasSameElementSetByRandomIdentifiers($solutionOrderingList)) { 528 throw new ilTestQuestionPoolException('inconsistent solution values given'); 529 } 530 531 return $solutionOrderingList; 532 } 533 534 /** 535 * @param $active_id 536 * @param $pass 537 * @return ilAssOrderingElementList 538 */ 539 public function getShuffledOrderingElementList() 540 { 541 $shuffledRandomIdentifierIndex = $this->getShuffler()->shuffle( 542 $this->getOrderingElementList()->getRandomIdentifierIndex() 543 ); 544 545 $shuffledElementList = $this->getOrderingElementList()->getClone(); 546 $shuffledElementList->reorderByRandomIdentifiers($shuffledRandomIdentifierIndex); 547 $shuffledElementList->resetElementsIndentations(); 548 549 return $shuffledElementList; 550 } 551 552 /** 553 * @return ilAssOrderingElementList 554 */ 555 public function getOrderingElementList() 556 { 557 return $this->orderingElementList; 558 } 559 560 /** 561 * @param ilAssOrderingElementList $orderingElementList 562 */ 563 public function setOrderingElementList($orderingElementList) 564 { 565 $this->orderingElementList = $orderingElementList; 566 } 567 568 /** 569 * @param $position 570 * 571 * TODO: still in use? should not since element moving is js supported!? 572 */ 573 public function moveAnswerUp($position) 574 { 575 if (!$this->getOrderingElementList()->elementExistByPosition($position)) { 576 return false; 577 } 578 579 if ($this->getOrderingElementList()->isFirstElementPosition($position)) { 580 return false; 581 } 582 583 $this->getOrderingElementList()->moveElementByPositions($position, $position - 1); 584 } 585 586 /** 587 * @param $position 588 * 589 * TODO: still in use? should not since element moving is js supported!? 590 */ 591 public function moveAnswerDown($position) 592 { 593 if (!$this->getOrderingElementList()->elementExistByPosition($position)) { 594 return false; 595 } 596 597 if ($this->getOrderingElementList()->isLastElementPosition($position)) { 598 return false; 599 } 600 601 $this->getOrderingElementList()->moveElementByPositions($position, $position + 1); 602 603 return true; 604 } 605 606 /** 607 * Returns the ordering element from the given position. 608 * 609 * @param int $position 610 * @return ilAssOrderingElement|null 611 */ 612 public function getAnswer($index = 0) 613 { 614 if (!$this->getOrderingElementList()->elementExistByPosition($index)) { 615 return null; 616 } 617 618 return $this->getOrderingElementList()->getElementByPosition($index); 619 } 620 621 /** 622 * Deletes an answer with a given index. The index of the first 623 * answer is 0, the index of the second answer is 1 and so on. 624 * 625 * @param integer $index A nonnegative index of the n-th answer 626 * @access public 627 * @see $answers 628 */ 629 public function deleteAnswer($randomIdentifier) 630 { 631 $this->getOrderingElementList()->removeElement( 632 $this->getOrderingElementList()->getElementByRandomIdentifier($randomIdentifier) 633 ); 634 $this->getOrderingElementList()->saveToDb(); 635 } 636 637 /** 638 * Returns the number of answers 639 * 640 * @return integer The number of answers of the ordering question 641 * @access public 642 * @see $answers 643 */ 644 public function getAnswerCount() 645 { 646 return $this->getOrderingElementList()->countElements(); 647 } 648 649 /** 650 * Returns the points, a learner has reached answering the question. 651 * The points are calculated from the given answers. 652 * 653 * @access public 654 * @param integer $active_id 655 * @param integer $pass 656 * @param boolean $returndetails (deprecated !!) 657 * @return integer/array $points/$details (array $details is deprecated !!) 658 */ 659 public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false) 660 { 661 if ($returndetails) { 662 throw new ilTestException('return details not implemented for ' . __METHOD__); 663 } 664 665 if (is_null($pass)) { 666 $pass = $this->getSolutionMaxPass($active_id); 667 } 668 669 $solutionValuePairs = $this->getSolutionValues($active_id, $pass, $authorizedSolution); 670 671 if (!count($solutionValuePairs)) { 672 return 0; 673 } 674 675 $indexedSolutionValues = $this->fetchIndexedValuesFromValuePairs($solutionValuePairs); 676 $solutionOrderingElementList = $this->getSolutionOrderingElementList($indexedSolutionValues); 677 678 return $this->calculateReachedPointsForSolution($solutionOrderingElementList); 679 } 680 681 public function calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $previewSession) 682 { 683 if (!$previewSession->hasParticipantSolution()) { 684 return 0; 685 } 686 687 $solutionOrderingElementList = unserialize( 688 $previewSession->getParticipantsSolution() 689 ); 690 691 $reachedPoints = $this->calculateReachedPointsForSolution($solutionOrderingElementList); 692 $reachedPoints = $this->deductHintPointsFromReachedPoints($previewSession, $reachedPoints); 693 694 return $this->ensureNonNegativePoints($reachedPoints); 695 } 696 697 /** 698 * Returns the maximum points, a learner can reach answering the question 699 * 700 * @return double Points 701 * @see $points 702 */ 703 public function getMaximumPoints() 704 { 705 return $this->getPoints(); 706 } 707 708 /* 709 * Returns the encrypted save filename of a matching picture 710 * Images are saved with an encrypted filename to prevent users from 711 * cheating by guessing the solution from the image filename 712 * 713 * @param string $filename Original filename 714 * @return string Encrypted filename 715 */ 716 public function getEncryptedFilename($filename) 717 { 718 $extension = ""; 719 if (preg_match("/.*\\.(\\w+)$/", $filename, $matches)) { 720 $extension = $matches[1]; 721 } 722 return md5($filename) . "." . $extension; 723 } 724 725 protected function cleanImagefiles() 726 { 727 if ($this->getOrderingType() == OQ_PICTURES) { 728 if (@file_exists($this->getImagePath())) { 729 $contents = ilUtil::getDir($this->getImagePath()); 730 foreach ($contents as $f) { 731 if (strcmp($f['type'], 'file') == 0) { 732 $found = false; 733 foreach ($this->getOrderingElementList() as $orderElement) { 734 if (strcmp($f['entry'], $orderElement->getContent()) == 0) { 735 $found = true; 736 } 737 if (strcmp($f['entry'], $this->getThumbPrefix() . $orderElement->getContent()) == 0) { 738 $found = true; 739 } 740 } 741 if (!$found) { 742 if (@file_exists($this->getImagePath() . $f['entry'])) { 743 @unlink($this->getImagePath() . $f['entry']); 744 } 745 } 746 } 747 } 748 } 749 } else { 750 if (@file_exists($this->getImagePath())) { 751 ilUtil::delDir($this->getImagePath()); 752 } 753 } 754 } 755 756 /* 757 * Deletes an imagefile from the system if the file is deleted manually 758 * 759 * @param string $filename Image file filename 760 * @return boolean Success 761 */ 762 public function dropImageFile($imageFilename) 763 { 764 if (!strlen($imageFilename)) { 765 return false; 766 } 767 768 $result = @unlink($this->getImagePath() . $imageFilename); 769 $result = $result & @unlink($this->getImagePath() . $this->getThumbPrefix() . $imageFilename); 770 771 return $result; 772 } 773 774 public function isImageFileStored($imageFilename) 775 { 776 if (!strlen($imageFilename)) { 777 return false; 778 } 779 780 if (!file_exists($this->getImagePath() . $imageFilename)) { 781 return false; 782 } 783 784 return is_file($this->getImagePath() . $imageFilename); 785 } 786 787 public function isImageReplaced(ilAssOrderingElement $newElement, ilAssOrderingElement $oldElement) 788 { 789 if (!$this->hasOrderingTypeUploadSupport()) { 790 return false; 791 } 792 793 if (!$newElement->getContent()) { 794 return false; 795 } 796 797 return $newElement->getContent() != $oldElement->getContent(); 798 } 799 800 /** 801 * Sets the image file and uploads the image to the object's image directory. 802 * 803 * @param string $image_filename Name of the original image file 804 * @param string $image_tempfilename Name of the temporary uploaded image file 805 * @return integer An errorcode if the image upload fails, 0 otherwise 806 * @access public 807 */ 808 public function storeImageFile($uploadFile, $targetFile) 809 { 810 if (!strlen($uploadFile)) { 811 return false; 812 } 813 814 $this->ensureImagePathExists(); 815 816 // store file with hashed name 817 818 if (!ilUtil::moveUploadedFile($uploadFile, $targetFile, $this->getImagePath() . $targetFile)) { 819 return false; 820 } 821 822 return true; 823 } 824 825 public function handleThumbnailCreation(ilAssOrderingElementList $elementList) 826 { 827 foreach ($elementList as $element) { 828 $this->createImageThumbnail($element); 829 } 830 } 831 832 public function createImageThumbnail(ilAssOrderingElement $element) 833 { 834 if ($this->getThumbGeometry()) { 835 $imageFile = $this->getImagePath() . $element->getContent(); 836 $thumbFile = $this->getImagePath() . $this->getThumbPrefix() . $element->getContent(); 837 838 ilUtil::convertImage($imageFile, $thumbFile, "JPEG", $this->getThumbGeometry()); 839 } 840 } 841 842 /** 843 * Checks the data to be saved for consistency 844 * 845 * @return boolean True, if the check was ok, False otherwise 846 * @access public 847 * @see $answers 848 */ 849 public function validateSolutionSubmit() 850 { 851 $submittedSolutionList = $this->getSolutionListFromPostSubmit(); 852 853 if (!$submittedSolutionList->hasElements()) { 854 return true; 855 } 856 857 return $this->getOrderingElementList()->hasSameElementSetByRandomIdentifiers($submittedSolutionList); 858 } 859 860 /** 861 * Saves the learners input of the question to the database. 862 * 863 * @access public 864 * @param integer $active_id Active id of the user 865 * @param integer $pass Test pass 866 * @return boolean $status 867 */ 868 public function saveWorkingData($active_id, $pass = null, $authorized = true) 869 { 870 $entered_values = 0; 871 872 if (is_null($pass)) { 873 include_once "./Modules/Test/classes/class.ilObjTest.php"; 874 $pass = ilObjTest::_getPass($active_id); 875 } 876 877 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation( 878 function () use (&$entered_values, $active_id, $pass, $authorized) { 879 $this->removeCurrentSolution($active_id, $pass, $authorized); 880 881 foreach ($this->getSolutionListFromPostSubmit() as $orderingElement) { 882 $value1 = $orderingElement->getStorageValue1($this->getOrderingType()); 883 $value2 = $orderingElement->getStorageValue2($this->getOrderingType()); 884 885 $this->saveCurrentSolution($active_id, $pass, $value1, trim($value2), $authorized); 886 887 $entered_values++; 888 } 889 } 890 ); 891 892 if ($entered_values) { 893 $this->log($active_id, 'log_user_entered_values'); 894 } else { 895 $this->log($active_id, 'log_user_not_entered_values'); 896 } 897 898 return true; 899 } 900 901 protected function savePreviewData(ilAssQuestionPreviewSession $previewSession) 902 { 903 if ($this->validateSolutionSubmit()) { 904 $previewSession->setParticipantsSolution(serialize($this->getSolutionListFromPostSubmit())); 905 } 906 } 907 908 public function saveAdditionalQuestionDataToDb() 909 { 910 /** @var ilDBInterface $ilDB */ 911 global $DIC; 912 $ilDB = $DIC['ilDB']; 913 914 // save additional data 915 $ilDB->manipulateF( 916 "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s", 917 array( "integer" ), 918 array( $this->getId() ) 919 ); 920 921 $ilDB->manipulateF( 922 "INSERT INTO " . $this->getAdditionalTableName() . " (question_fi, ordering_type, thumb_geometry, element_height) 923 VALUES (%s, %s, %s, %s)", 924 array( "integer", "text", "integer", "integer" ), 925 array( 926 $this->getId(), 927 $this->ordering_type, 928 $this->getThumbGeometry(), 929 ($this->getElementHeight() > 20) ? $this->getElementHeight() : null 930 ) 931 ); 932 } 933 934 public function saveAnswerSpecificDataToDb() 935 { 936 $this->getOrderingElementList()->setQuestionId($this->getId()); 937 $this->getOrderingElementList()->saveToDb(); 938 939 if ($this->hasOrderingTypeUploadSupport()) { 940 $this->rebuildThumbnails(); 941 $this->cleanImagefiles(); 942 } 943 } 944 945 /** 946 * Returns the question type of the question 947 * 948 * @return integer The question type of the question 949 * @access public 950 */ 951 public function getQuestionType() 952 { 953 return "assOrderingQuestion"; 954 } 955 956 /** 957 * Returns the name of the additional question data table in the database 958 * 959 * @return string The additional table name 960 * @access public 961 */ 962 public function getAdditionalTableName() 963 { 964 return "qpl_qst_ordering"; 965 } 966 967 /** 968 * Returns the name of the answer table in the database 969 * 970 * @return string The answer table name 971 * @access public 972 */ 973 public function getAnswerTableName() 974 { 975 return "qpl_a_ordering"; 976 } 977 978 /** 979 * Collects all text in the question which could contain media objects 980 * which were created with the Rich Text Editor 981 */ 982 public function getRTETextWithMediaObjects() 983 { 984 $text = parent::getRTETextWithMediaObjects(); 985 986 foreach ($this->getOrderingElementList() as $orderingElement) { 987 $text .= $orderingElement->getContent(); 988 } 989 990 return $text; 991 } 992 993 /** 994 * Returns the answers array 995 * @deprecated seriously, stop looking for this kind data at this point (!) look where it comes from and learn (!) 996 */ 997 public function getOrderElements() 998 { 999 return $this->getOrderingElementList()->getRandomIdentifierIndexedElements(); 1000 } 1001 1002 /** 1003 * Returns true if the question type supports JavaScript output 1004 * 1005 * @return boolean TRUE if the question type supports JavaScript output, FALSE otherwise 1006 * @access public 1007 */ 1008 public function supportsJavascriptOutput() 1009 { 1010 return true; 1011 } 1012 1013 public function supportsNonJsOutput() 1014 { 1015 return false; 1016 } 1017 1018 /** 1019 * {@inheritdoc} 1020 */ 1021 public function setExportDetailsXLS($worksheet, $startrow, $active_id, $pass) 1022 { 1023 parent::setExportDetailsXLS($worksheet, $startrow, $active_id, $pass); 1024 1025 $solutions = $this->getSolutionValues($active_id, $pass); 1026 $sol = array(); 1027 foreach ($solutions as $solution) { 1028 $sol[$solution["value1"]] = $solution["value2"]; 1029 } 1030 asort($sol); 1031 $sol = array_keys($sol); 1032 1033 $i = 1; 1034 foreach ($sol as $idx) { 1035 foreach ($solutions as $solution) { 1036 if ($solution["value1"] == $idx) { 1037 $worksheet->setCell($startrow + $i, 0, $solution["value2"]); 1038 } 1039 } 1040 $element = $this->getOrderingElementList()->getElementBySolutionIdentifier($idx); 1041 $worksheet->setCell($startrow + $i, 1, $element->getContent()); 1042 $i++; 1043 } 1044 1045 return $startrow + $i + 1; 1046 } 1047 1048 /* 1049 * Get the thumbnail geometry 1050 * 1051 * @return integer Geometry 1052 */ 1053 public function getThumbGeometry() 1054 { 1055 return $this->thumb_geometry; 1056 } 1057 1058 public function getThumbSize() 1059 { 1060 return $this->getThumbGeometry(); 1061 } 1062 1063 /* 1064 * Set the thumbnail geometry 1065 * 1066 * @param integer $a_geometry Geometry 1067 */ 1068 public function setThumbGeometry($a_geometry) 1069 { 1070 $this->thumb_geometry = ($a_geometry < 1) ? 100 : $a_geometry; 1071 } 1072 1073 /* 1074 * Get the minimum element height 1075 * 1076 * @return integer Height 1077 */ 1078 public function getElementHeight() 1079 { 1080 return $this->element_height; 1081 } 1082 1083 /* 1084 * Set the minimum element height 1085 * 1086 * @param integer $a_height Height 1087 */ 1088 public function setElementHeight($a_height) 1089 { 1090 $this->element_height = ($a_height < 20) ? "" : $a_height; 1091 } 1092 1093 /* 1094 * Rebuild the thumbnail images with a new thumbnail size 1095 */ 1096 public function rebuildThumbnails() 1097 { 1098 if ($this->getOrderingType() == OQ_PICTURES || $this->getOrderingType() == OQ_NESTED_PICTURES) { 1099 foreach ($this->getOrderElements() as $orderingElement) { 1100 $this->generateThumbForFile($this->getImagePath(), $orderingElement->getContent()); 1101 } 1102 } 1103 } 1104 1105 public function getThumbPrefix() 1106 { 1107 return "thumb."; 1108 } 1109 1110 protected function generateThumbForFile($path, $file) 1111 { 1112 $filename = $path . $file; 1113 if (@file_exists($filename)) { 1114 $thumbpath = $path . $this->getThumbPrefix() . $file; 1115 $path_info = @pathinfo($filename); 1116 $ext = ""; 1117 switch (strtoupper($path_info['extension'])) { 1118 case 'PNG': 1119 $ext = 'PNG'; 1120 break; 1121 case 'GIF': 1122 $ext = 'GIF'; 1123 break; 1124 default: 1125 $ext = 'JPEG'; 1126 break; 1127 } 1128 ilUtil::convertImage($filename, $thumbpath, $ext, $this->getThumbGeometry()); 1129 } 1130 } 1131 1132 /** 1133 * Returns a JSON representation of the question 1134 */ 1135 public function toJSON() 1136 { 1137 include_once("./Services/RTE/classes/class.ilRTE.php"); 1138 $result = array(); 1139 $result['id'] = (int) $this->getId(); 1140 $result['type'] = (string) $this->getQuestionType(); 1141 $result['title'] = (string) $this->getTitle(); 1142 $result['question'] = $this->formatSAQuestion($this->getQuestion()); 1143 $result['nr_of_tries'] = (int) $this->getNrOfTries(); 1144 $result['shuffle'] = (bool) true; 1145 $result['points'] = $this->getPoints(); 1146 $result['feedback'] = array( 1147 'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)), 1148 'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true)) 1149 ); 1150 if ($this->getOrderingType() == OQ_PICTURES) { 1151 $result['path'] = $this->getImagePathWeb(); 1152 } 1153 1154 $counter = 1; 1155 $answers = array(); 1156 foreach ($this->getOrderingElementList() as $orderingElement) { 1157 $answers[$counter] = $orderingElement->getContent(); 1158 $counter++; 1159 } 1160 $answers = $this->getShuffler()->shuffle($answers); 1161 $arr = array(); 1162 foreach ($answers as $order => $answer) { 1163 array_push($arr, array( 1164 "answertext" => (string) $answer, 1165 "order" => (int) $order 1166 )); 1167 } 1168 $result['answers'] = $arr; 1169 1170 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId()); 1171 $result['mobs'] = $mobs; 1172 1173 return json_encode($result); 1174 } 1175 1176 /** 1177 * @return ilAssNestedOrderingElementsInputGUI|ilAssOrderingImagesInputGUI|ilAssOrderingTextsInputGUI 1178 * @throws ilTestQuestionPoolException 1179 */ 1180 public function buildOrderingElementInputGui() 1181 { 1182 switch ($this->getOrderingType()) { 1183 case OQ_TERMS: 1184 1185 return $this->buildOrderingTextsInputGui(); 1186 1187 case OQ_PICTURES: 1188 1189 return $this->buildOrderingImagesInputGui(); 1190 1191 case OQ_NESTED_TERMS: 1192 case OQ_NESTED_PICTURES: 1193 1194 return $this->buildNestedOrderingElementInputGui(); 1195 1196 default: 1197 throw new ilTestQuestionPoolException('unknown ordering mode'); 1198 } 1199 } 1200 1201 /** 1202 * @param ilAssOrderingTextsInputGUI|ilAssOrderingImagesInputGUI|ilAssNestedOrderingElementsInputGUI $formField 1203 */ 1204 public function initOrderingElementAuthoringProperties(ilFormPropertyGUI $formField) 1205 { 1206 switch (true) { 1207 case $formField instanceof ilAssNestedOrderingElementsInputGUI: 1208 1209 $formField->setInteractionEnabled(true); 1210 $formField->setNestingEnabled($this->isOrderingTypeNested()); 1211 break; 1212 1213 case $formField instanceof ilAssOrderingTextsInputGUI: 1214 case $formField instanceof ilAssOrderingImagesInputGUI: 1215 default: 1216 1217 $formField->setEditElementOccuranceEnabled(true); 1218 $formField->setEditElementOrderEnabled(true); 1219 } 1220 1221 $formField->setRequired(true); 1222 } 1223 1224 /** 1225 * @param ilFormPropertyGUI $formField 1226 */ 1227 public function initOrderingElementFormFieldLabels(ilFormPropertyGUI $formField) 1228 { 1229 $formField->setInfo($this->lng->txt('ordering_answer_sequence_info')); 1230 $formField->setTitle($this->lng->txt('answers')); 1231 } 1232 1233 /** 1234 * @return ilAssOrderingTextsInputGUI 1235 */ 1236 public function buildOrderingTextsInputGui() 1237 { 1238 $formDataConverter = $this->buildOrderingTextsFormDataConverter(); 1239 1240 require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssOrderingTextsInputGUI.php'; 1241 1242 $orderingElementInput = new ilAssOrderingTextsInputGUI( 1243 $formDataConverter, 1244 self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR 1245 ); 1246 1247 $this->initOrderingElementFormFieldLabels($orderingElementInput); 1248 1249 return $orderingElementInput; 1250 } 1251 1252 /** 1253 * @return ilAssOrderingImagesInputGUI 1254 */ 1255 public function buildOrderingImagesInputGui() 1256 { 1257 $formDataConverter = $this->buildOrderingImagesFormDataConverter(); 1258 1259 require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssOrderingImagesInputGUI.php'; 1260 1261 $orderingElementInput = new ilAssOrderingImagesInputGUI( 1262 $formDataConverter, 1263 self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR 1264 ); 1265 1266 $orderingElementInput->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG); 1267 $orderingElementInput->setImageUploadCommand(self::ORDERING_ELEMENT_FORM_CMD_UPLOAD_IMG); 1268 1269 $this->initOrderingElementFormFieldLabels($orderingElementInput); 1270 1271 return $orderingElementInput; 1272 } 1273 1274 /** 1275 * @return ilAssNestedOrderingElementsInputGUI 1276 */ 1277 public function buildNestedOrderingElementInputGui() 1278 { 1279 $formDataConverter = $this->buildNestedOrderingFormDataConverter(); 1280 1281 require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssNestedOrderingElementsInputGUI.php'; 1282 1283 $orderingElementInput = new ilAssNestedOrderingElementsInputGUI( 1284 $formDataConverter, 1285 self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR 1286 ); 1287 1288 $orderingElementInput->setUniquePrefix($this->getId()); 1289 $orderingElementInput->setOrderingType($this->getOrderingType()); 1290 $orderingElementInput->setElementImagePath($this->getImagePathWeb()); 1291 $orderingElementInput->setThumbPrefix($this->getThumbPrefix()); 1292 1293 $this->initOrderingElementFormFieldLabels($orderingElementInput); 1294 1295 return $orderingElementInput; 1296 } 1297 1298 /** 1299 * @param ilPropertyFormGUI $form 1300 * @return ilAssOrderingElementList $submittedElementList 1301 */ 1302 public function fetchSolutionListFromSubmittedForm(ilPropertyFormGUI $form) 1303 { 1304 return $form->getItemByPostVar(self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR)->getElementList($this->getId()); 1305 } 1306 1307 /** 1308 * @param array $userSolutionPost 1309 * @return ilAssOrderingElementList 1310 * @throws ilTestException 1311 */ 1312 public function fetchSolutionListFromFormSubmissionData($userSolutionPost) 1313 { 1314 $orderingGUI = $this->buildNestedOrderingElementInputGui(); 1315 $orderingGUI->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_USER_SOLUTION_SUBMISSION); 1316 $orderingGUI->setValueByArray($userSolutionPost); 1317 1318 if (!$orderingGUI->checkInput()) { 1319 require_once 'Modules/Test/exceptions/class.ilTestException.php'; 1320 throw new ilTestException('error on validating user solution post'); 1321 } 1322 1323 require_once 'Modules/TestQuestionPool/classes/questions/class.ilAssOrderingElementList.php'; 1324 $solutionOrderingElementList = ilAssOrderingElementList::buildInstance($this->getId()); 1325 1326 $storedElementList = $this->getOrderingElementList(); 1327 1328 foreach ($orderingGUI->getElementList($this->getId()) as $submittedElement) { 1329 $solutionElement = $storedElementList->getElementByRandomIdentifier( 1330 $submittedElement->getRandomIdentifier() 1331 )->getClone(); 1332 1333 $solutionElement->setPosition($submittedElement->getPosition()); 1334 1335 if ($this->isOrderingTypeNested()) { 1336 $solutionElement->setIndentation($submittedElement->getIndentation()); 1337 } 1338 1339 $solutionOrderingElementList->addElement($solutionElement); 1340 } 1341 1342 return $solutionOrderingElementList; 1343 } 1344 1345 /** 1346 * @var ilAssOrderingElementList 1347 */ 1348 private $postSolutionOrderingElementList = null; 1349 1350 /** 1351 * @return ilAssOrderingElementList 1352 */ 1353 public function getSolutionListFromPostSubmit() 1354 { 1355 if ($this->postSolutionOrderingElementList === null) { 1356 $list = $this->fetchSolutionListFromFormSubmissionData($_POST); 1357 $this->postSolutionOrderingElementList = $list; 1358 } 1359 1360 return $this->postSolutionOrderingElementList; 1361 } 1362 1363 /** 1364 * @return array 1365 */ 1366 public function getSolutionPostSubmit() 1367 { 1368 return $this->fetchSolutionSubmit($_POST); 1369 } 1370 1371 /** 1372 * @param $user_order 1373 * @param $nested_solution 1374 * @return int 1375 */ 1376 protected function calculateReachedPointsForSolution(ilAssOrderingElementList $solutionOrderingElementList) 1377 { 1378 $reachedPoints = $this->getPoints(); 1379 1380 foreach ($this->getOrderingElementList() as $correctElement) { 1381 $userElement = $solutionOrderingElementList->getElementByPosition($correctElement->getPosition()); 1382 1383 if (!$correctElement->isSameElement($userElement)) { 1384 $reachedPoints = 0; 1385 break; 1386 } 1387 } 1388 1389 return $reachedPoints; 1390 } 1391 1392 /*** 1393 * @param object $child 1394 * @param integer $ordering_depth 1395 * @param bool $with_random_id 1396 */ 1397 public function getLeveledOrdering() 1398 { 1399 return $this->leveled_ordering; 1400 } 1401 1402 public function getOldLeveledOrdering() 1403 { 1404 global $DIC; 1405 $ilDB = $DIC['ilDB']; 1406 1407 $res = $ilDB->queryF( 1408 'SELECT depth FROM qpl_a_ordering WHERE question_fi = %s ORDER BY position ASC', 1409 array('integer'), 1410 array($this->getId()) 1411 ); 1412 while ($row = $ilDB->fetchAssoc($res)) { 1413 $this->old_ordering_depth[] = $row['depth']; 1414 } 1415 return $this->old_ordering_depth; 1416 } 1417 1418 /*** 1419 * @param integer $a_random_id 1420 * @return integer 1421 */ 1422 public function lookupSolutionOrderByRandomid($a_random_id) 1423 { 1424 global $DIC; 1425 $ilDB = $DIC['ilDB']; 1426 1427 $res = $ilDB->queryF( 1428 'SELECT solution_key FROM qpl_a_ordering WHERE random_id = %s', 1429 array('integer'), 1430 array($a_random_id) 1431 ); 1432 $row = $ilDB->fetchAssoc($res); 1433 1434 return $row['solution_key']; 1435 } 1436 1437 public function updateLeveledOrdering($a_index, $a_answer_text, $a_depth) 1438 { 1439 global $DIC; 1440 $ilDB = $DIC['ilDB']; 1441 1442 $ilDB->update( 1443 'qpl_a_ordering', 1444 array('solution_key' => array('integer', $a_index), 1445 'depth' => array('integer', $a_depth)), 1446 array('answertext' => array('text', $a_answer_text)) 1447 ); 1448 1449 1450 return true; 1451 } 1452 1453 /** 1454 * Get all available operations for a specific question 1455 * 1456 * @param string $expression 1457 * 1458 * @internal param string $expression_type 1459 * @return array 1460 */ 1461 public function getOperators($expression) 1462 { 1463 require_once "./Modules/TestQuestionPool/classes/class.ilOperatorsExpressionMapping.php"; 1464 return ilOperatorsExpressionMapping::getOperatorsByExpression($expression); 1465 } 1466 1467 /** 1468 * Get all available expression types for a specific question 1469 * @return array 1470 */ 1471 public function getExpressionTypes() 1472 { 1473 return array( 1474 iQuestionCondition::PercentageResultExpression, 1475 iQuestionCondition::NumericResultExpression, 1476 iQuestionCondition::OrderingResultExpression, 1477 iQuestionCondition::EmptyAnswerExpression, 1478 ); 1479 } 1480 1481 /** 1482 * Get the user solution for a question by active_id and the test pass 1483 * 1484 * @param int $active_id 1485 * @param int $pass 1486 * 1487 * @return ilUserQuestionResult 1488 */ 1489 public function getUserQuestionResult($active_id, $pass) 1490 { 1491 /** @var ilDBInterface $ilDB */ 1492 global $DIC; 1493 $ilDB = $DIC['ilDB']; 1494 $result = new ilUserQuestionResult($this, $active_id, $pass); 1495 1496 $maxStep = $this->lookupMaxStep($active_id, $pass); 1497 1498 if ($maxStep !== null) { 1499 $data = $ilDB->queryF( 1500 "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s ORDER BY value1 ASC ", 1501 array("integer", "integer", "integer","integer"), 1502 array($active_id, $pass, $this->getId(), $maxStep) 1503 ); 1504 } else { 1505 $data = $ilDB->queryF( 1506 "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s ORDER BY value1 ASC ", 1507 array("integer", "integer", "integer"), 1508 array($active_id, $pass, $this->getId()) 1509 ); 1510 } 1511 1512 $elements = array(); 1513 while ($row = $ilDB->fetchAssoc($data)) { 1514 $newKey = explode(":", $row["value2"]); 1515 1516 foreach ($this->getOrderingElementList() as $answer) { 1517 // Images nut supported 1518 if (!$this->isOrderingTypeNested()) { 1519 if ($answer->getSolutionIdentifier() == $row["value1"]) { 1520 $elements[$row["value2"]] = $answer->getSolutionIdentifier() + 1; 1521 break; 1522 } 1523 } else { 1524 if ($answer->getRandomIdentifier() == $newKey[0]) { 1525 $elements[$row["value1"]] = $answer->getSolutionIdentifier() + 1; 1526 break; 1527 } 1528 } 1529 } 1530 } 1531 1532 ksort($elements); 1533 1534 foreach (array_values($elements) as $element) { 1535 $result->addKeyValue($element, $element); 1536 } 1537 1538 $points = $this->calculateReachedPoints($active_id, $pass); 1539 $max_points = $this->getMaximumPoints(); 1540 1541 $result->setReachedPercentage(($points / $max_points) * 100); 1542 1543 return $result; 1544 } 1545 1546 /** 1547 * If index is null, the function returns an array with all anwser options 1548 * Else it returns the specific answer option 1549 * 1550 * @param null|int $index 1551 * 1552 * @return array|ASS_AnswerSimple 1553 */ 1554 public function getAvailableAnswerOptions($index = null) 1555 { 1556 if ($index !== null) { 1557 return $this->getOrderingElementList()->getElementByPosition($index); 1558 } 1559 1560 return $this->getOrderingElementList()->getElements(); 1561 } 1562 1563 /** 1564 * {@inheritdoc} 1565 */ 1566 protected function afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId) 1567 { 1568 parent::afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId); 1569 $this->duplicateImages($dupQuestionId, $dupParentObjId, $origQuestionId, $origParentObjId); 1570 } 1571 1572 // fau: testNav - new function getTestQuestionConfig() 1573 /** 1574 * Get the test question configuration 1575 * @return ilTestQuestionConfig 1576 */ 1577 // hey: refactored identifiers 1578 public function buildTestPresentationConfig() 1579 // hey. 1580 { 1581 // hey: refactored identifiers 1582 return parent::buildTestPresentationConfig() 1583 // hey. 1584 ->setIsUnchangedAnswerPossible(true) 1585 ->setUseUnchangedAnswerLabel($this->lng->txt('tst_unchanged_order_is_correct')); 1586 } 1587 // fau. 1588 1589 protected function ensureImagePathExists() 1590 { 1591 if (!file_exists($this->getImagePath())) { 1592 ilUtil::makeDirParents($this->getImagePath()); 1593 } 1594 } 1595 1596 /** 1597 * @return array 1598 */ 1599 public function fetchSolutionSubmit($formSubmissionDataStructure) 1600 { 1601 $solutionSubmit = array(); 1602 1603 if (isset($formSubmissionDataStructure['orderresult'])) { 1604 $orderresult = $formSubmissionDataStructure['orderresult']; 1605 1606 if (strlen($orderresult)) { 1607 $orderarray = explode(":", $orderresult); 1608 $ordervalue = 1; 1609 foreach ($orderarray as $index) { 1610 $idmatch = null; 1611 if (preg_match("/id_(\\d+)/", $index, $idmatch)) { 1612 $randomid = $idmatch[1]; 1613 foreach ($this->getOrderingElementList() as $answeridx => $answer) { 1614 if ($answer->getRandomIdentifier() == $randomid) { 1615 $solutionSubmit[$answeridx] = $ordervalue; 1616 $ordervalue++; 1617 } 1618 } 1619 } 1620 } 1621 } 1622 } elseif ($this->getOrderingType() == OQ_NESTED_TERMS || $this->getOrderingType() == OQ_NESTED_PICTURES) { 1623 $index = 0; 1624 foreach ($formSubmissionDataStructure['content'] as $randomId => $content) { 1625 $indentation = $formSubmissionDataStructure['indentation']; 1626 1627 $value1 = $index++; 1628 $value2 = implode(':', array($randomId, $indentation)); 1629 1630 $solutionSubmit[$value1] = $value2; 1631 } 1632 } else { 1633 foreach ($formSubmissionDataStructure as $key => $value) { 1634 $matches = null; 1635 if (preg_match("/^order_(\d+)/", $key, $matches)) { 1636 if (!(preg_match("/initial_value_\d+/", $value))) { 1637 if (strlen($value)) { 1638 foreach ($this->getOrderingElementList() as $answeridx => $answer) { 1639 if ($answer->getRandomIdentifier() == $matches[1]) { 1640 $solutionSubmit[$answeridx] = $value; 1641 } 1642 } 1643 } 1644 } 1645 } 1646 } 1647 } 1648 1649 return $solutionSubmit; 1650 } 1651 1652 /** 1653 * @return ilAssOrderingFormValuesObjectsConverter 1654 */ 1655 protected function buildOrderingElementFormDataConverter() 1656 { 1657 require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssOrderingFormValuesObjectsConverter.php'; 1658 $converter = new ilAssOrderingFormValuesObjectsConverter(); 1659 $converter->setPostVar(self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR); 1660 1661 return $converter; 1662 } 1663 1664 /** 1665 * @return ilAssOrderingFormValuesObjectsConverter 1666 */ 1667 protected function buildOrderingImagesFormDataConverter() 1668 { 1669 $formDataConverter = $this->buildOrderingElementFormDataConverter(); 1670 $formDataConverter->setContext(ilAssOrderingFormValuesObjectsConverter::CONTEXT_MAINTAIN_ELEMENT_IMAGE); 1671 1672 $formDataConverter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG); 1673 $formDataConverter->setImageUrlPath($this->getImagePathWeb()); 1674 $formDataConverter->setImageFsPath($this->getImagePath()); 1675 1676 if ($this->getThumbSize() && $this->getThumbPrefix()) { 1677 $formDataConverter->setThumbnailPrefix($this->getThumbPrefix()); 1678 } 1679 return $formDataConverter; 1680 } 1681 1682 /** 1683 * @return ilAssOrderingFormValuesObjectsConverter 1684 */ 1685 protected function buildOrderingTextsFormDataConverter() 1686 { 1687 $formDataConverter = $this->buildOrderingElementFormDataConverter(); 1688 $formDataConverter->setContext(ilAssOrderingFormValuesObjectsConverter::CONTEXT_MAINTAIN_ELEMENT_TEXT); 1689 return $formDataConverter; 1690 } 1691 1692 /** 1693 * @return ilAssOrderingFormValuesObjectsConverter 1694 */ 1695 protected function buildNestedOrderingFormDataConverter() 1696 { 1697 $formDataConverter = $this->buildOrderingElementFormDataConverter(); 1698 $formDataConverter->setContext(ilAssOrderingFormValuesObjectsConverter::CONTEXT_MAINTAIN_HIERARCHY); 1699 1700 if ($this->getOrderingType() == OQ_NESTED_PICTURES) { 1701 $formDataConverter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG); 1702 $formDataConverter->setImageUrlPath($this->getImagePathWeb()); 1703 1704 if ($this->getThumbSize() && $this->getThumbPrefix()) { 1705 $formDataConverter->setThumbnailPrefix($this->getThumbPrefix()); 1706 } 1707 } 1708 1709 return $formDataConverter; 1710 } 1711} 1712