ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.assOrderingQuestion.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
25
41{
42 public const ORDERING_ELEMENT_FORM_FIELD_POSTVAR = 'order_elems';
43
44 public const ORDERING_ELEMENT_FORM_CMD_UPLOAD_IMG = 'uploadElementImage';
45 public const ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG = 'removeElementImage'; //might actually go away - use ORDERING_ELEMENT_FORM_CMD_UPLOAD_IMG
46
47 public const OQ_PICTURES = 0;
48 public const OQ_TERMS = 1;
49 public const OQ_NESTED_PICTURES = 2;
50 public const OQ_NESTED_TERMS = 3;
51
52 public const OQ_CT_PICTURES = 'pics';
53 public const OQ_CT_TERMS = 'terms';
54
55 public const VALID_UPLOAD_SUFFIXES = ["jpg", "jpeg", "png", "gif"];
56 protected const HAS_SPECIFIC_FEEDBACK = false;
57
58 public ?int $element_height = null;
60 public $leveled_ordering = [];
61 protected ?OQRepository $oq_repository = null;
62
64
65 public function __construct(
66 string $title = "",
67 string $comment = "",
68 string $author = "",
69 int $owner = -1,
70 string $question = "",
71 protected int $ordering_type = self::OQ_TERMS
72 ) {
74 }
75
76 public function isComplete(): bool
77 {
78 $elements_list = $this->getOrderingElementList()->getElements();
79 if ($elements_list === [] && $this->element_list_for_deferred_saving !== null) {
80 $elements_list = $this->element_list_for_deferred_saving->getElements();
81 }
82 $elements = array_filter(
83 $elements_list,
84 fn($element) => trim($element->getContent()) != ''
85 );
86
87 $has_at_least_two_elements = count($elements) > 1;
88
89 $complete = $this->getAuthor()
90 && $this->getTitle()
91 && $this->getQuestion()
92 && $this->getMaximumPoints()
93 && $has_at_least_two_elements;
94
95 return $complete;
96 }
97
98 protected function getRepository(): OQRepository
99 {
100 if (is_null($this->oq_repository)) {
101 $this->oq_repository = new OQRepository($this->db);
102 }
104 }
105
106 public function saveToDb(?int $original_id = null): void
107 {
110 parent::saveToDb();
111 if ($this->element_list_for_deferred_saving !== null) {
112 $this->setOrderingElementList($this->element_list_for_deferred_saving);
113 }
114 }
115
123 public function loadFromDb($question_id): void
124 {
125 $result = $this->db->queryF(
126 "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",
127 ["integer"],
128 [$question_id]
129 );
130 if ($result->numRows() == 1) {
131 $data = $this->db->fetchAssoc($result);
132 $this->setId($question_id);
133 $this->setObjId($data["obj_fi"]);
134 $this->setTitle((string) $data["title"]);
135 $this->setComment((string) $data["description"]);
136 $this->setOriginalId($data["original_id"]);
137 $this->setAuthor($data["author"]);
138 $this->setNrOfTries($data['nr_of_tries']);
139 $this->setPoints($data["points"]);
140 $this->setOwner($data["owner"]);
141 $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc((string) $data["question_text"], 1));
142 $this->ordering_type = $data["ordering_type"] !== null ? (int) $data["ordering_type"] : self::OQ_TERMS;
143 if ($data['thumb_geometry'] !== null && $data['thumb_geometry'] >= $this->getMinimumThumbSize()) {
144 $this->setThumbSize($data['thumb_geometry']);
145 }
146 $this->element_height = $data["element_height"] ? (int) $data['element_height'] : null;
147
148 try {
149 $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
152 }
153
154 try {
155 $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
157 }
158 }
159
160 parent::loadFromDb($question_id);
161 }
162
164 \assQuestion $target
165 ): \assQuestion {
166 $list = $this->getRepository()->getOrderingList($this->getId())
167 ->withQuestionId($target->getId());
168 $list->distributeNewRandomIdentifiers();
169 $target->setOrderingElementList($list);
170 $this->cloneImages($this->getId(), $this->getObjId(), $target->getId(), $target->getObjId());
171 $target->saveToDb();
172 return $target;
173 }
174
175 public function cloneImages(
176 int $source_question_id,
177 int $source_parent_id,
178 int $target_question_id,
179 int $target_parent_id
180 ): void {
181 if (!$this->isImageOrderingType()) {
182 return;
183 }
184
185 $image_source_path = $this->getImagePath($source_question_id, $source_parent_id);
186 $image_target_path = $this->getImagePath($target_question_id, $target_parent_id);
187
188 if (!file_exists($image_target_path)) {
189 ilFileUtils::makeDirParents($image_target_path);
190 } else {
191 $this->removeAllImageFiles($image_target_path);
192 }
193 foreach ($this->getOrderingElementList() as $element) {
194 $filename = $element->getContent();
195
196 if ($filename === '') {
197 continue;
198 }
199
200 if (!file_exists($image_source_path . $filename)
201 || !copy($image_source_path . $filename, $image_target_path . $filename)) {
202 $this->log->root()->warning('Image could not be cloned for object for question: ' . $target_question_id);
203 }
204 if (!file_exists($image_source_path . $this->getThumbPrefix() . $filename)
205 || !copy($image_source_path . $this->getThumbPrefix() . $filename, $image_target_path . $this->getThumbPrefix() . $filename)) {
206 $this->log->root()->warning('Image thumbnails could not be cloned for object for question: ' . $target_question_id);
207 }
208 }
209 }
210
211 protected function getValidOrderingTypes(): array
212 {
213 return [
214 self::OQ_PICTURES,
215 self::OQ_TERMS,
216 self::OQ_NESTED_PICTURES,
217 self::OQ_NESTED_TERMS
218 ];
219 }
220
221 public function setOrderingType(int $ordering_type = self::OQ_TERMS)
222 {
223 if (!in_array($ordering_type, $this->getValidOrderingTypes())) {
224 throw new \InvalidArgumentException('Must be valid ordering type.');
225 }
226 $this->ordering_type = $ordering_type;
227 }
228
229 public function getOrderingType(): int
230 {
231 return $this->ordering_type;
232 }
233
234 public function isOrderingTypeNested(): bool
235 {
236 $nested = [
237 self::OQ_NESTED_TERMS,
238 self::OQ_NESTED_PICTURES
239 ];
240 return in_array($this->getOrderingType(), $nested);
241 }
242
243 public function isImageOrderingType(): bool
244 {
245 $with_images = [
246 self::OQ_PICTURES,
247 self::OQ_NESTED_PICTURES
248 ];
249 return in_array($this->getOrderingType(), $with_images);
250 }
251
252 public function setContentType($ct)
253 {
254 if (!in_array($ct, [
255 self::OQ_CT_PICTURES,
256 self::OQ_CT_TERMS
257 ])) {
258 throw new \InvalidArgumentException("use OQ content-type", 1);
259 }
260 if ($ct == self::OQ_CT_PICTURES) {
261 if ($this->isOrderingTypeNested()) {
262 $this->setOrderingType(self::OQ_NESTED_PICTURES);
263 } else {
264 $this->setOrderingType(self::OQ_PICTURES);
265 }
266 $this->setThumbSize($this->getThumbSize());
267 }
268 if ($ct == self::OQ_CT_TERMS) {
269 if ($this->isOrderingTypeNested()) {
270 $this->setOrderingType(self::OQ_NESTED_TERMS);
271 } else {
272 $this->setOrderingType(self::OQ_TERMS);
273 }
274 }
275 }
276
277 public function setNestingType(bool $nesting)
278 {
279 if ($nesting) {
280 if ($this->isImageOrderingType()) {
281 $this->setOrderingType(self::OQ_NESTED_PICTURES);
282 } else {
283 $this->setOrderingType(self::OQ_NESTED_TERMS);
284 }
285 } else {
286 if ($this->isImageOrderingType()) {
287 $this->setOrderingType(self::OQ_PICTURES);
288 } else {
289 $this->setOrderingType(self::OQ_TERMS);
290 }
291 }
292 }
293
294 public function hasOrderingTypeUploadSupport(): bool
295 {
296 return $this->isImageOrderingType();
297 }
298
300 bool $force_correct_solution,
301 int $active_id,
302 ?int $pass_index
304 if ($force_correct_solution || !$active_id || $pass_index === null) {
305 return $this->getOrderingElementList();
306 }
307
308 $solution_values = $this->getSolutionValues($active_id, $pass_index);
309
310 if (!count($solution_values)) {
311 return $this->getShuffledOrderingElementList();
312 }
313
314 return $this->getSolutionOrderingElementList($this->fetchIndexedValuesFromValuePairs($solution_values));
315 }
316
319 array $last_post,
320 int $active_id,
321 int $pass
323 if ($input_gui->isPostSubmit($last_post)) {
324 return $this->fetchSolutionListFromFormSubmissionData($last_post);
325 }
326 $indexedSolutionValues = $this->fetchIndexedValuesFromValuePairs(
327 // hey: prevPassSolutions - obsolete due to central check
328 $this->getTestOutputSolutions($active_id, $pass)
329 // hey.
330 );
331
332 if (count($indexedSolutionValues)) {
333 return $this->getSolutionOrderingElementList($indexedSolutionValues);
334 }
335
336 return $this->getShuffledOrderingElementList();
337 }
338
340 int $value1,
341 string $value2
343 $value = explode(':', $value2);
344
345 $random_identifier = (int) $value[0];
346 $selected_position = $value1;
347 $selected_indentation = (int) $value[1];
348
349 $element = $this->getOrderingElementList()->getElementByRandomIdentifier($random_identifier)->getClone();
350
351 $element->setPosition($selected_position);
352 $element->setIndentation($selected_indentation);
353
354 return $element;
355 }
356
358 int $value1,
359 string $value2
361 $solution_identifier = $value1;
362 $selected_position = ($value2 - 1);
363 $selected_indentation = 0;
364
365 $element = $this->getOrderingElementList()->getElementBySolutionIdentifier($solution_identifier)->getClone();
366
367 $element->setPosition($selected_position);
368 $element->setIndentation($selected_indentation);
369
370 return $element;
371 }
372
376 public function getSolutionOrderingElementList(array $indexed_solution_values): ilAssOrderingElementList
377 {
378 $solution_ordering_list = new ilAssOrderingElementList();
379 $solution_ordering_list->setQuestionId($this->getId());
380
381 foreach ($indexed_solution_values as $value1 => $value2) {
382 if ($this->isOrderingTypeNested()) {
383 $element = $this->getSolutionValuePairBrandedOrderingElementByRandomIdentifier($value1, $value2);
384 } else {
385 $element = $this->getSolutionValuePairBrandedOrderingElementBySolutionIdentifier($value1, $value2);
386 }
387
388 $solution_ordering_list->addElement($element);
389 }
390
391 if (!$this->getOrderingElementList()->hasSameElementSetByRandomIdentifiers($solution_ordering_list)) {
392 throw new ilTestQuestionPoolException('inconsistent solution values given');
393 }
394
395 return $solution_ordering_list;
396 }
397
404 {
405 $shuffledRandomIdentifierIndex = $this->getShuffler()->transform(
406 $this->getOrderingElementList()->getRandomIdentifierIndex()
407 );
408
409 $shuffledElementList = $this->getOrderingElementList()->getClone();
410 $shuffledElementList->reorderByRandomIdentifiers($shuffledRandomIdentifierIndex);
411 $shuffledElementList->resetElementsIndentations();
412
413 return $shuffledElementList;
414 }
415
420 {
421 return $this->getRepository()->getOrderingList($this->getId());
422 }
423
428 {
429 if ($this->getId() <= 0) {
430 $this->element_list_for_deferred_saving = $list;
431 return;
432 }
433 $list = $list->withQuestionId($this->getId());
434 $elements = $list->getElements();
435 $nu = [];
436 foreach ($elements as $e) {
437 $nu[] = $list->ensureValidIdentifiers($e);
438 }
439 $this->getRepository()->updateOrderingList(
440 $list->withElements($nu)
441 );
442 }
443
450 public function getAnswer(int $index = 0): ?ilAssOrderingElement
451 {
452 if (!$this->getOrderingElementList()->elementExistByPosition($index)) {
453 return null;
454 }
455
456 return $this->getOrderingElementList()->getElementByPosition($index);
457 }
458
459 public function deleteAnswer(int $random_identifier): void
460 {
461 $this->getOrderingElementList()->removeElement(
462 $this->getOrderingElementList()->getElementByRandomIdentifier($random_identifier)
463 );
464 $this->getOrderingElementList()->saveToDb();
465 }
466
467 public function getAnswerCount(): int
468 {
469 return $this->getOrderingElementList()->countElements();
470 }
471
472 public function calculateReachedPoints(
473 int $active_id,
474 ?int $pass = null,
475 bool $authorized_solution = true
476 ): float {
477 if ($pass === null) {
478 $pass = $this->getSolutionMaxPass($active_id);
479 }
480
481 $solution_value_pairs = $this->getSolutionValues($active_id, $pass, $authorized_solution);
482
483 if ($solution_value_pairs === []) {
484 return 0.0;
485 }
486
487 $solution_ordering_element_list = $this->getSolutionOrderingElementList(
488 $this->fetchIndexedValuesFromValuePairs($solution_value_pairs)
489 );
490
491 return $this->calculateReachedPointsForSolution($solution_ordering_element_list);
492 }
493
495 {
496 if (!$preview_session->hasParticipantSolution()) {
497 return 0.0;
498 }
499
500 $solution_ordering_element_list = unserialize(
501 $preview_session->getParticipantsSolution(),
502 ['allowed_classes' => true]
503 );
504
505 $reached_points = $this->calculateReachedPointsForSolution($solution_ordering_element_list);
506
507 return $this->ensureNonNegativePoints($reached_points);
508 }
509
510 public function getMaximumPoints(): float
511 {
512 return $this->getPoints();
513 }
514
515 /*
516 * Returns the encrypted save filename of a matching picture
517 * Images are saved with an encrypted filename to prevent users from
518 * cheating by guessing the solution from the image filename
519 *
520 * @param string $filename Original filename
521 * @return string Encrypted filename
522 */
523 public function getEncryptedFilename($filename): string
524 {
525 $extension = "";
526 if (preg_match("/.*\\.(\\w+)$/", $filename, $matches)) {
527 $extension = $matches[1];
528 }
529 return md5($filename) . "." . $extension;
530 }
531
532 protected function cleanImagefiles(): void
533 {
534 if ($this->getOrderingType() == self::OQ_PICTURES) {
535 if (@file_exists($this->getImagePath())) {
536 $contents = ilFileUtils::getDir($this->getImagePath());
537 foreach ($contents as $f) {
538 if (strcmp($f['type'], 'file') == 0) {
539 $found = false;
540 foreach ($this->getOrderingElementList() as $orderElement) {
541 if (strcmp($f['entry'], $orderElement->getContent()) == 0) {
542 $found = true;
543 }
544 if (strcmp($f['entry'], $this->getThumbPrefix() . $orderElement->getContent()) == 0) {
545 $found = true;
546 }
547 }
548 if (!$found) {
549 if (@file_exists($this->getImagePath() . $f['entry'])) {
550 @unlink($this->getImagePath() . $f['entry']);
551 }
552 }
553 }
554 }
555 }
556 } else {
557 if (@file_exists($this->getImagePath())) {
558 ilFileUtils::delDir($this->getImagePath());
559 }
560 }
561 }
562
563 /*
564 * Deletes an imagefile from the system if the file is deleted manually
565 *
566 * @param string $filename Image file filename
567 * @return boolean Success
568 */
569 public function dropImageFile($imageFilename)
570 {
571 if (!strlen($imageFilename)) {
572 return false;
573 }
574
575 $result = @unlink($this->getImagePath() . $imageFilename);
576 $result = $result && @unlink($this->getImagePath() . $this->getThumbPrefix() . $imageFilename);
577
578 return $result;
579 }
580
581 public function isImageFileStored($imageFilename): bool
582 {
583 if (!strlen($imageFilename)) {
584 return false;
585 }
586
587 if (!file_exists($this->getImagePath() . $imageFilename)) {
588 return false;
589 }
590
591 return is_file($this->getImagePath() . $imageFilename);
592 }
593
594 public function isImageReplaced(ilAssOrderingElement $newElement, ilAssOrderingElement $oldElement): bool
595 {
596 if (!$this->hasOrderingTypeUploadSupport()) {
597 return false;
598 }
599
600 if (!$newElement->getContent()) {
601 return false;
602 }
603
604 return $newElement->getContent() != $oldElement->getContent();
605 }
606
607
608 public function storeImageFile(string $upload_file, string $upload_name): ?string
609 {
610 $name_parts = explode(".", $upload_name);
611 $suffix = strtolower(array_pop($name_parts));
612 if (!in_array($suffix, self::VALID_UPLOAD_SUFFIXES)) {
613 return null;
614 }
615
616 $this->ensureImagePathExists();
617 $target_filename = $this->buildHashedImageFilename($upload_name, true);
618 $target_filepath = $this->getImagePath() . $target_filename;
619 if (ilFileUtils::moveUploadedFile($upload_file, $target_filename, $target_filepath)) {
620 $thumb_path = $this->getImagePath() . $this->getThumbPrefix() . $target_filename;
621 ilShellUtil::convertImage($target_filepath, $thumb_path, "JPEG", (string) $this->getThumbSize());
622
623 return $target_filename;
624 }
625
626 return null;
627 }
628
629 public function updateImageFile(string $existing_image_name): ?string
630 {
631 $existing_image_path = $this->getImagePath() . $existing_image_name;
632 $target_filename = $this->buildHashedImageFilename($existing_image_name, true);
633 $target_filepath = $this->getImagePath() . $target_filename;
634 if (ilFileUtils::rename($existing_image_path, $target_filepath)) {
635 unlink($this->getImagePath() . $this->getThumbPrefix() . $existing_image_name);
636 $thumb_path = $this->getImagePath() . $this->getThumbPrefix() . $target_filename;
637 ilShellUtil::convertImage($target_filepath, $thumb_path, "JPEG", (string) $this->getThumbSize());
638
639 return $target_filename;
640 }
641
642 return $existing_image_name;
643 }
644
645 public function validateSolutionSubmit(): bool
646 {
647 $submittedSolutionList = $this->getSolutionListFromPostSubmit();
648
649 if (!$submittedSolutionList->hasElements()) {
650 return true;
651 }
652
653 return $this->getOrderingElementList()->hasSameElementSetByRandomIdentifiers($submittedSolutionList);
654 }
655
656 public function saveWorkingData(
657 int $active_id,
658 ?int $pass = null,
659 bool $authorized = true
660 ): bool {
661 if ($this->questionpool_request->raw('test_answer_changed') === null) {
662 return true;
663 }
664
665 if (is_null($pass)) {
666 $pass = ilObjTest::_getPass($active_id);
667 }
668
669 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
670 function () use ($active_id, $pass, $authorized) {
671 $this->removeCurrentSolution($active_id, $pass, $authorized);
672
673 foreach ($this->getSolutionListFromPostSubmit() as $orderingElement) {
674 $value1 = $orderingElement->getStorageValue1($this->getOrderingType());
675 $value2 = $orderingElement->getStorageValue2($this->getOrderingType());
676
677 $this->saveCurrentSolution($active_id, $pass, $value1, trim((string) $value2), $authorized);
678 }
679 }
680 );
681
682 return true;
683 }
684
685 protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
686 {
687 if ($this->validateSolutionSubmit()) {
688 $previewSession->setParticipantsSolution(serialize($this->getSolutionListFromPostSubmit()));
689 }
690 }
691
693 {
694 // save additional data
695 $this->db->manipulateF(
696 "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
697 ["integer"],
698 [$this->getId()]
699 );
700
701 $this->db->manipulateF(
702 "INSERT INTO " . $this->getAdditionalTableName() . " (question_fi, ordering_type, thumb_geometry, element_height)
703 VALUES (%s, %s, %s, %s)",
704 ["integer", "text", "integer", "integer"],
705 [
706 $this->getId(),
707 $this->ordering_type,
708 $this->getThumbSize(),
709 ($this->getElementHeight() > 20) ? $this->getElementHeight() : null
710 ]
711 );
712 }
713
714
715 protected function getQuestionRepository(): OQRepository
716 {
717 return new OQRepository($this->db);
718 }
719
721 {
722 }
723
730 public function getQuestionType(): string
731 {
732 return "assOrderingQuestion";
733 }
734
741 public function getAdditionalTableName(): string
742 {
743 return "qpl_qst_ordering";
744 }
745
752 public function getAnswerTableName(): string
753 {
754 return "qpl_a_ordering";
755 }
756
761 public function getRTETextWithMediaObjects(): string
762 {
763 $text = parent::getRTETextWithMediaObjects();
764
765 foreach ($this->getOrderingElementList() as $orderingElement) {
766 $text .= $orderingElement->getContent();
767 }
768
769 return $text;
770 }
771
776 public function getOrderElements(): array
777 {
778 return $this->getOrderingElementList()->getRandomIdentifierIndexedElements();
779 }
780
781 public function getElementHeight(): ?int
782 {
783 return $this->element_height;
784 }
785
786 public function setElementHeight(?int $a_height): void
787 {
788 $this->element_height = ($a_height < 20) ? null : $a_height;
789 }
790
791 /*
792 * Rebuild the thumbnail images with a new thumbnail size
793 */
794 public function rebuildThumbnails(): void
795 {
796 if ($this->isImageOrderingType()) {
797 foreach ($this->getOrderElements() as $orderingElement) {
798 if ($orderingElement->getContent() !== '') {
799 $this->generateThumbForFile($this->getImagePath(), $orderingElement->getContent());
800 }
801 }
802 }
803 }
804
805 public function getThumbPrefix(): string
806 {
807 return "thumb.";
808 }
809
810 protected function generateThumbForFile($path, $file): void
811 {
812 $filename = $path . $file;
813 if (@file_exists($filename)) {
814 $thumbpath = $path . $this->getThumbPrefix() . $file;
815 $path_info = @pathinfo($filename);
816 $ext = "";
817 switch (strtoupper($path_info['extension'])) {
818 case 'PNG':
819 $ext = 'PNG';
820 break;
821 case 'GIF':
822 $ext = 'GIF';
823 break;
824 default:
825 $ext = 'JPEG';
826 break;
827 }
828 ilShellUtil::convertImage($filename, $thumbpath, $ext, (string) $this->getThumbSize());
829 }
830 }
831
835 public function toJSON(): string
836 {
837 $result = [];
838 $result['id'] = $this->getId();
839 $result['type'] = (string) $this->getQuestionType();
840 $result['title'] = $this->getTitleForHTMLOutput();
841 $result['question'] = $this->formatSAQuestion($this->getQuestion());
842 $result['nr_of_tries'] = $this->getNrOfTries();
843 $result['shuffle'] = true;
844 $result['points'] = $this->getPoints();
845 $result['feedback'] = [
846 'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
847 'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
848 ];
849 if ($this->getOrderingType() == self::OQ_PICTURES) {
850 $result['path'] = $this->getImagePathWeb();
851 }
852
853 $counter = 1;
854 $answers = [];
855 foreach ($this->getOrderingElementList() as $orderingElement) {
856 $answers[$counter] = $orderingElement->getContent();
857 $counter++;
858 }
859 $answers = $this->getShuffler()->transform($answers);
860 $arr = [];
861 foreach ($answers as $order => $answer) {
862 array_push($arr, [
863 "answertext" => (string) $answer,
864 "order" => (int) $order
865 ]);
866 }
867 $result['answers'] = $arr;
868
869 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
870 $result['mobs'] = $mobs;
871
872 return json_encode($result);
873 }
874
880 {
881 if ($this->isImageOrderingType()) {
882 return $this->buildOrderingImagesInputGui();
883 }
884 return $this->buildOrderingTextsInputGui();
885 }
886
887
892 {
893 switch (true) {
894 case $formField instanceof ilAssNestedOrderingElementsInputGUI:
895 $formField->setInteractionEnabled(true);
896 $formField->setNestingEnabled($this->isOrderingTypeNested());
897 break;
898
899 case $formField instanceof ilAssOrderingTextsInputGUI:
900 case $formField instanceof ilAssOrderingImagesInputGUI:
901 default:
902
903 $formField->setEditElementOccuranceEnabled(true);
904 $formField->setEditElementOrderEnabled(true);
905 }
906 }
907
912 {
913 $formField->setInfo($this->lng->txt('ordering_answer_sequence_info'));
914 $formField->setTitle($this->lng->txt('answers'));
915 }
916
921 {
922 $formDataConverter = $this->buildOrderingTextsFormDataConverter();
923
924 $orderingElementInput = new ilAssOrderingTextsInputGUI(
925 $formDataConverter,
926 self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
927 );
928
929 $this->initOrderingElementFormFieldLabels($orderingElementInput);
930 $orderingElementInput->setInfo($orderingElementInput->getInfo() . ' ' . $this->lng->txt('latex_edit_info'));
931
932 return $orderingElementInput;
933 }
934
939 {
940 $formDataConverter = $this->buildOrderingImagesFormDataConverter();
941
942 $orderingElementInput = new ilAssOrderingImagesInputGUI(
943 $formDataConverter,
944 self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
945 );
946
947 $orderingElementInput->setImageUploadCommand(self::ORDERING_ELEMENT_FORM_CMD_UPLOAD_IMG);
948 $orderingElementInput->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
949
950 $this->initOrderingElementFormFieldLabels($orderingElementInput);
951
952 return $orderingElementInput;
953 }
954
959 {
960 $form_data_converter = $this->buildNestedOrderingFormDataConverter();
961
962 $ordering_element_input = new ilAssNestedOrderingElementsInputGUI(
963 $form_data_converter,
964 self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
965 );
966
967 $ordering_element_input->setUniquePrefix($this->getId());
968 $ordering_element_input->setOrderingType($this->getOrderingType());
969 $ordering_element_input->setElementImagePath($this->getImagePathWeb());
970 $ordering_element_input->setThumbPrefix($this->getThumbPrefix());
971
972 $this->initOrderingElementFormFieldLabels($ordering_element_input);
973
974 return $ordering_element_input;
975 }
976
977 public function fetchSolutionListFromFormSubmissionData(array $user_solution_post): ilAssOrderingElementList
978 {
979 $ordering_gui = $this->buildNestedOrderingElementInputGui();
981 $ordering_gui->setValueByArray($user_solution_post);
982
983 if (!$ordering_gui->checkInput()) {
984 throw new ilTestException('error on validating user solution post');
985 }
986
987 $solution_ordering_element_list = ilAssOrderingElementList::buildInstance($this->getId());
988
989 $stored_element_list = $this->getOrderingElementList();
990
991 foreach ($ordering_gui->getElementList($this->getId()) as $submitted_element) {
992 $solution_element = $stored_element_list->getElementByRandomIdentifier(
993 $submitted_element->getRandomIdentifier()
994 )?->getClone();
995
996 if ($solution_element === null) {
997 continue;
998 }
999
1000 $solution_element->setPosition($submitted_element->getPosition());
1001
1002 if ($this->isOrderingTypeNested()) {
1003 $solution_element->setIndentation($submitted_element->getIndentation());
1004 }
1005
1006 $solution_ordering_element_list->addElement($solution_element);
1007 }
1008
1009 return $solution_ordering_element_list;
1010 }
1011
1012 private ?ilAssOrderingElementList $postSolutionOrderingElementList = null;
1013
1015 {
1016 if ($this->postSolutionOrderingElementList === null) {
1017 $list = $this->fetchSolutionListFromFormSubmissionData(
1018 $this->http->request()->getParsedBody()
1019 );
1020 $this->postSolutionOrderingElementList = $list;
1021 }
1022
1023 return $this->postSolutionOrderingElementList;
1024 }
1025
1026 protected function calculateReachedPointsForSolution(ilAssOrderingElementList $solution_ordering_element_list): float
1027 {
1028 $reached_points = $this->getPoints();
1029
1030 foreach ($this->getOrderingElementList() as $correct_element) {
1031 $user_element = $solution_ordering_element_list->getElementByPosition($correct_element->getPosition());
1032 if (!$correct_element->isSameElement($user_element)) {
1033 $reached_points = 0;
1034 break;
1035 }
1036 }
1037
1038 return $reached_points;
1039 }
1040
1041 public function getOperators(string $expression): array
1042 {
1044 }
1045
1046 public function getExpressionTypes(): array
1047 {
1048 return [
1053 ];
1054 }
1055
1056 public function getUserQuestionResult(
1057 int $active_id,
1058 int $pass
1060 $result = new ilUserQuestionResult($this, $active_id, $pass);
1061
1062 $maxStep = $this->lookupMaxStep($active_id, $pass);
1063 if ($maxStep > 0) {
1064 $data = $this->db->queryF(
1065 "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s ORDER BY value1 ASC ",
1066 ["integer", "integer", "integer","integer"],
1067 [$active_id, $pass, $this->getId(), $maxStep]
1068 );
1069 } else {
1070 $data = $this->db->queryF(
1071 "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s ORDER BY value1 ASC ",
1072 ["integer", "integer", "integer"],
1073 [$active_id, $pass, $this->getId()]
1074 );
1075 }
1076
1077 $elements = [];
1078 while ($row = $this->db->fetchAssoc($data)) {
1079 $newKey = explode(":", $row["value2"]);
1080
1081 foreach ($this->getOrderingElementList() as $answer) {
1082 // Images not supported
1083 if (!$this->isOrderingTypeNested()) {
1084 if ($answer->getSolutionIdentifier() == $row["value1"]) {
1085 $elements[$row["value2"]] = $answer->getSolutionIdentifier() + 1;
1086 break;
1087 }
1088 } else {
1089 if ($answer->getRandomIdentifier() == $newKey[0]) {
1090 $elements[$row["value1"]] = $answer->getSolutionIdentifier() + 1;
1091 break;
1092 }
1093 }
1094 }
1095 }
1096
1097 ksort($elements);
1098
1099 foreach (array_values($elements) as $element) {
1100 $result->addKeyValue($element, $element);
1101 }
1102
1103 $points = $this->calculateReachedPoints($active_id, $pass);
1104 $max_points = $this->getMaximumPoints();
1105
1106 $result->setReachedPercentage(($points / $max_points) * 100);
1107
1108 return $result;
1109 }
1110
1118 public function getAvailableAnswerOptions($index = null)
1119 {
1120 if ($index !== null) {
1121 return $this->getOrderingElementList()->getElementByPosition($index);
1122 }
1123
1124 return $this->getOrderingElementList()->getElements();
1125 }
1126
1127 // fau: testNav - new function getTestQuestionConfig()
1132 // hey: refactored identifiers
1134 // hey.
1135 {
1136 // hey: refactored identifiers
1137 return parent::buildTestPresentationConfig()
1138 // hey.
1140 ->setUseUnchangedAnswerLabel($this->lng->txt('tst_unchanged_order_is_correct'));
1141 }
1142 // fau.
1143
1144 protected function ensureImagePathExists(): void
1145 {
1146 if (!file_exists($this->getImagePath())) {
1147 ilFileUtils::makeDirParents($this->getImagePath());
1148 }
1149 }
1150
1154 public function fetchSolutionSubmit(array $form_submission_data_structure): array
1155 {
1156 $solution_submit = [];
1157
1158 if (isset($form_submission_data_structure['orderresult'])) {
1159 $orderresult = $form_submission_data_structure['orderresult'];
1160
1161 if (strlen($orderresult)) {
1162 $orderarray = explode(":", $orderresult);
1163 $ordervalue = 1;
1164 foreach ($orderarray as $index) {
1165 $idmatch = null;
1166 if (preg_match("/id_(\\d+)/", $index, $idmatch)) {
1167 $randomid = $idmatch[1];
1168 foreach ($this->getOrderingElementList() as $answeridx => $answer) {
1169 if ($answer->getRandomIdentifier() == $randomid) {
1170 $solution_submit[$answeridx] = $ordervalue;
1171 $ordervalue++;
1172 }
1173 }
1174 }
1175 }
1176 }
1177 } elseif ($this->getOrderingType() == self::OQ_NESTED_TERMS || $this->getOrderingType() == self::OQ_NESTED_PICTURES) {
1178 $index = 0;
1179 foreach ($form_submission_data_structure['content'] as $randomId => $content) {
1180 $indentation = $form_submission_data_structure['indentation'];
1181
1182 $value1 = $index++;
1183 $value2 = implode(':', [$randomId, $indentation]);
1184
1185 $solution_submit[$value1] = $value2;
1186 }
1187 } else {
1188 foreach ($form_submission_data_structure as $key => $value) {
1189 $matches = null;
1190 if (preg_match("/^order_(\d+)/", $key, $matches)) {
1191 if (!(preg_match("/initial_value_\d+/", $value))) {
1192 if (strlen($value)) {
1193 foreach ($this->getOrderingElementList() as $answeridx => $answer) {
1194 if ($answer->getRandomIdentifier() == $matches[1]) {
1195 $solution_submit[$answeridx] = $value;
1196 }
1197 }
1198 }
1199 }
1200 }
1201 }
1202 }
1203
1204 return $solution_submit;
1205 }
1206
1211 {
1212 $converter = new ilAssOrderingFormValuesObjectsConverter();
1213 $converter->setPostVar(self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR);
1214
1215 return $converter;
1216 }
1217
1222 {
1223 $formDataConverter = $this->buildOrderingElementFormDataConverter();
1225
1226 $formDataConverter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
1227 $formDataConverter->setImageUrlPath($this->getImagePathWeb());
1228 $formDataConverter->setImageFsPath($this->getImagePath());
1229
1230 if ($this->getThumbPrefix()) {
1231 $formDataConverter->setThumbnailPrefix($this->getThumbPrefix());
1232 }
1233 return $formDataConverter;
1234 }
1235
1240 {
1241 $form_data_converter = $this->buildOrderingElementFormDataConverter();
1243 return $form_data_converter;
1244 }
1245
1250 {
1251 $form_data_converter = $this->buildOrderingElementFormDataConverter();
1253
1254 if ($this->getOrderingType() === self::OQ_NESTED_PICTURES) {
1255 $form_data_converter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
1256 $form_data_converter->setImageUrlPath($this->getImagePathWeb());
1257 $form_data_converter->setThumbnailPrefix($this->getThumbPrefix());
1258 }
1259
1260 return $form_data_converter;
1261 }
1262
1263 public function toLog(AdditionalInformationGenerator $additional_info): array
1264 {
1265 return [
1266 AdditionalInformationGenerator::KEY_QUESTION_TYPE => (string) $this->getQuestionType(),
1267 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $this->getTitleForHTMLOutput(),
1268 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $this->formatSAQuestion($this->getQuestion()),
1269 AdditionalInformationGenerator::KEY_QUESTION_ORDERING_NESTING_TYPE => array_reduce(
1270 $this->getOrderingTypeLangVars($this->getOrderingType()),
1271 static fn(string $string, string $lang_var) => $string . $additional_info->getTagForLangVar($lang_var),
1272 ''
1273 ),
1274 AdditionalInformationGenerator::KEY_QUESTION_REACHABLE_POINTS => $this->getPoints(),
1275 AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION => $this->getSolutionOutputForLog(),
1276 AdditionalInformationGenerator::KEY_FEEDBACK => [
1277 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_INCOMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1278 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_COMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1279 ]
1280 ];
1281 }
1282
1283 private function getOrderingTypeLangVars(int $ordering_type): array
1284 {
1285 switch ($ordering_type) {
1286 case self::OQ_PICTURES:
1287 return ['qst_nested_nested_answers_off', 'oq_btn_use_order_pictures'];
1288 case self::OQ_TERMS:
1289 return ['qst_nested_nested_answers_off', 'oq_btn_use_order_terms'];
1290 case self::OQ_NESTED_PICTURES:
1291 return ['qst_nested_nested_answers_on', 'oq_btn_use_order_pictures'];
1292 case self::OQ_NESTED_TERMS:
1293 return ['qst_nested_nested_answers_on', 'oq_btn_use_order_terms'];
1294 default:
1295 return ['', ''];
1296 }
1297 }
1298
1299 private function getSolutionOutputForLog(): string
1300 {
1301 $solution_ordering_list = $this->getOrderingElementList();
1302
1303 $answers_gui = $this->buildNestedOrderingElementInputGui();
1305 $answers_gui->setInteractionEnabled(false);
1306 $answers_gui->setElementList($solution_ordering_list);
1307
1308 return $answers_gui->getHTML();
1309 }
1310
1311 protected function solutionValuesToLog(
1312 AdditionalInformationGenerator $additional_info,
1313 array $solution_values
1314 ): array {
1315 if ($solution_values === []) {
1316 return [];
1317 }
1318 return $this->getElementArrayWithIdentationsForTextOutput(
1319 $this->getSolutionOrderingElementList(
1320 $this->fetchIndexedValuesFromValuePairs($solution_values)
1321 )->getElements()
1322 );
1323 }
1324
1325 public function solutionValuesToText(array $solution_values): array
1326 {
1327 if ($solution_values === []) {
1328 return [];
1329 }
1330 return $this->getElementArrayWithIdentationsForTextOutput(
1331 $this->getSolutionOrderingElementList(
1332 $this->fetchIndexedValuesFromValuePairs($solution_values)
1333 )->getElements()
1334 );
1335 }
1336
1337 public function getCorrectSolutionForTextOutput(int $active_id, int $pass): array
1338 {
1339 return $this->getElementArrayWithIdentationsForTextOutput(
1340 $this->getOrderingElementList()->getElements()
1341 );
1342 }
1343
1349 private function getElementArrayWithIdentationsForTextOutput(array $elements): array
1350 {
1351 usort(
1352 $elements,
1354 => $a->getPosition() - $b->getPosition()
1355 );
1356
1357 return array_map(
1358 function (ilAssOrderingElement $v): string {
1359 $indentation = '';
1360 for ($i = 0;$i < $v->getIndentation();$i++) {
1361 $indentation .= ' |';
1362 }
1363 return $indentation . $v->getContent();
1364 },
1365 $elements
1366 );
1367 }
1368}
$filename
Definition: buildRTE.php:78
repository for assOrderingQuestion (the answer elements within, at least...)
Class for ordering questions.
getOrderElements()
Returns the answers array.
saveWorkingData(int $active_id, ?int $pass=null, bool $authorized=true)
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $preview_session)
fetchSolutionSubmit(array $form_submission_data_structure)
getExpressionTypes()
Get all available expression types for a specific question.
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
saveToDb(?int $original_id=null)
getQuestionType()
Returns the question type of the question.
getOrderingElementListForSolutionOutput(bool $force_correct_solution, int $active_id, ?int $pass_index)
__construct(string $title="", string $comment="", string $author="", int $owner=-1, string $question="", protected int $ordering_type=self::OQ_TERMS)
calculateReachedPoints(int $active_id, ?int $pass=null, bool $authorized_solution=true)
initOrderingElementAuthoringProperties(ilFormPropertyGUI $formField)
getAnswerTableName()
Returns the name of the answer table in the database.
fetchSolutionListFromFormSubmissionData(array $user_solution_post)
getOperators(string $expression)
Get all available operations for a specific question.
getSolutionValuePairBrandedOrderingElementByRandomIdentifier(int $value1, string $value2)
toJSON()
Returns a JSON representation of the question.
setOrderingType(int $ordering_type=self::OQ_TERMS)
solutionValuesToLog(AdditionalInformationGenerator $additional_info, array $solution_values)
MUST convert the given solution values into an array or a string that can be stored in the log.
getSolutionOrderingElementList(array $indexed_solution_values)
isImageReplaced(ilAssOrderingElement $newElement, ilAssOrderingElement $oldElement)
solutionValuesToText(array $solution_values)
MUST convert the given solution values into text.
getSolutionOrderingElementListForTestOutput(ilAssNestedOrderingElementsInputGUI $input_gui, array $last_post, int $active_id, int $pass)
buildTestPresentationConfig()
Get the test question configuration.
cloneImages(int $source_question_id, int $source_parent_id, int $target_question_id, int $target_parent_id)
savePreviewData(ilAssQuestionPreviewSession $previewSession)
ilAssOrderingElementList $element_list_for_deferred_saving
storeImageFile(string $upload_file, string $upload_name)
toLog(AdditionalInformationGenerator $additional_info)
MUST return an array of the question settings that can be stored in the log.
getUserQuestionResult(int $active_id, int $pass)
Get the user solution for a question by active_id and the test pass.
initOrderingElementFormFieldLabels(ilFormPropertyGUI $formField)
getCorrectSolutionForTextOutput(int $active_id, int $pass)
getElementArrayWithIdentationsForTextOutput(array $elements)
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
getAdditionalTableName()
Returns the name of the additional question data table in the database.
getSolutionValuePairBrandedOrderingElementBySolutionIdentifier(int $value1, string $value2)
cloneQuestionTypeSpecificProperties(\assQuestion $target)
loadFromDb($question_id)
Loads a assOrderingQuestion object from a database.
setOrderingElementList(ilAssOrderingElementList $list)
calculateReachedPointsForSolution(ilAssOrderingElementList $solution_ordering_element_list)
deleteAnswer(int $random_identifier)
getOrderingTypeLangVars(int $ordering_type)
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
updateImageFile(string $existing_image_name)
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
getAnswer(int $index=0)
Returns the ordering element from the given position.
setOriginalId(?int $original_id)
setId(int $id=-1)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
saveToDb(?int $original_id=null)
setQuestion(string $question="")
setAuthor(string $author="")
setThumbSize(int $a_size)
setComment(string $comment="")
setObjId(int $obj_id=0)
setOwner(int $owner=-1)
setNrOfTries(int $a_nr_of_tries)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
setTitle(string $title="")
saveQuestionDataToDb(?int $original_id=null)
setPoints(float $points)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static buildInstance(int $question_id, array $elements=[])
ensureValidIdentifiers(ilAssOrderingElement $element)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
static getDir(string $a_dir, bool $a_rec=false, ?string $a_sub_dir="")
get directory
static rename(string $a_source, string $a_target)
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
This class represents a property in a property form.
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
static getOperatorsByExpression(string $expression)
static _replaceMediaObjectImageSrc(string $a_text, int $a_direction=0, string $nic='')
Replaces image source from mob image urls with the mob id or replaces mob id with the correct image s...
static convertImage(string $a_from, string $a_to, string $a_target_format="", string $a_geometry="", string $a_background_color="")
Base Exception for all Exceptions relating to Modules/Test.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setIsUnchangedAnswerPossible($isUnchangedAnswerPossible)
Set if the saving of an unchanged answer is supported with an additional checkbox.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$path
Definition: ltiservices.php:30
static http()
Fetches the global http state from ILIAS.
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
if(!file_exists('../ilias.ini.php'))
$counter