ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.assMatchingQuestion.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
24use ILIAS\Refinery\Random\Group as RandomGroup;
26
41{
42 public const MT_TERMS_PICTURES = 0;
43 public const MT_TERMS_DEFINITIONS = 1;
44
45 public const MATCHING_MODE_1_ON_1 = '1:1';
46 public const MATCHING_MODE_N_ON_N = 'n:n';
47
48 public int $thumb_geometry = 100;
49 private int $shufflemode = 0;
50 public int $element_height;
51 public int $matching_type;
53
54 private RandomGroup $randomGroup;
55
64
68 protected array $terms = [];
69
73 protected array $definitions = [];
74
87 public function __construct(
88 $title = "",
89 $comment = "",
90 $author = "",
91 $owner = -1,
92 $question = "",
93 $matching_type = self::MT_TERMS_DEFINITIONS
94 ) {
95 global $DIC;
96
98 $this->matchingpairs = [];
99 $this->matching_type = $matching_type;
100 $this->terms = [];
101 $this->definitions = [];
102 $this->randomGroup = $DIC->refinery()->random();
103 }
104
105 public function getShuffleMode(): int
106 {
107 return $this->shufflemode;
108 }
109
110 public function setShuffleMode(int $shuffle)
111 {
112 $this->shufflemode = $shuffle;
113 }
114
115 public function isComplete(): bool
116 {
117 if (strlen($this->title)
118 && $this->author
119 && $this->question
120 && count($this->matchingpairs)
121 && $this->getMaximumPoints() > 0
122 ) {
123 return true;
124 }
125 return false;
126 }
127
128 public function saveToDb(?int $original_id = null): void
129 {
133
134 parent::saveToDb();
135 }
136
138 {
139 $this->rebuildThumbnails();
140
141 $this->db->manipulateF(
142 "DELETE FROM qpl_a_mterm WHERE question_fi = %s",
143 [ 'integer' ],
144 [ $this->getId() ]
145 );
146
147 // delete old definitions
148 $this->db->manipulateF(
149 "DELETE FROM qpl_a_mdef WHERE question_fi = %s",
150 [ 'integer' ],
151 [ $this->getId() ]
152 );
153
154 $termids = [];
155 // write terms
156 foreach ($this->terms as $key => $term) {
157 $next_id = $this->db->nextId('qpl_a_mterm');
158 $this->db->insert('qpl_a_mterm', [
159 'term_id' => ['integer', $next_id],
160 'question_fi' => ['integer', $this->getId()],
161 'picture' => ['text', $term->getPicture()],
162 'term' => ['text', $term->getText()],
163 'ident' => ['integer', $term->getIdentifier()]
164 ]);
165 $termids[$term->getIdentifier()] = $next_id;
166 }
167
168 $definitionids = [];
169 // write definitions
170 foreach ($this->definitions as $key => $definition) {
171 $next_id = $this->db->nextId('qpl_a_mdef');
172 $this->db->insert('qpl_a_mdef', [
173 'def_id' => ['integer', $next_id],
174 'question_fi' => ['integer', $this->getId()],
175 'picture' => ['text', $definition->getPicture()],
176 'definition' => ['text', $definition->getText()],
177 'ident' => ['integer', $definition->getIdentifier()]
178 ]);
179 $definitionids[$definition->getIdentifier()] = $next_id;
180 }
181
182 $this->db->manipulateF(
183 "DELETE FROM qpl_a_matching WHERE question_fi = %s",
184 [ 'integer' ],
185 [ $this->getId() ]
186 );
188 foreach ($matchingpairs as $key => $pair) {
189 $next_id = $this->db->nextId('qpl_a_matching');
190 $this->db->manipulateF(
191 "INSERT INTO qpl_a_matching (answer_id, question_fi, points, term_fi, definition_fi) VALUES (%s, %s, %s, %s, %s)",
192 [ 'integer', 'integer', 'float', 'integer', 'integer' ],
193 [
194 $next_id,
195 $this->getId(),
196 $pair->getPoints(),
197 $termids[$pair->getTerm()->getIdentifier()],
198 $definitionids[$pair->getDefinition()->getIdentifier()]
199 ]
200 );
201 }
202 }
203
205 {
206 $this->db->manipulateF(
207 "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
208 [ "integer" ],
209 [ $this->getId() ]
210 );
211
212 $this->db->insert($this->getAdditionalTableName(), [
213 'question_fi' => ['integer', $this->getId()],
214 'shuffle' => ['text', $this->getShuffleMode()],
215 'matching_type' => ['text', $this->matching_type],
216 'thumb_geometry' => ['integer', $this->getThumbGeometry()],
217 'matching_mode' => ['text', $this->getMatchingMode()]
218 ]);
219 }
220
227 public function loadFromDb($question_id): void
228 {
229 $query = "
230 SELECT qpl_questions.*,
231 {$this->getAdditionalTableName()}.*
232 FROM qpl_questions
233 LEFT JOIN {$this->getAdditionalTableName()}
234 ON {$this->getAdditionalTableName()}.question_fi = qpl_questions.question_id
235 WHERE qpl_questions.question_id = %s
236 ";
237
238 $result = $this->db->queryF(
239 $query,
240 ['integer'],
241 [$question_id]
242 );
243
244 if ($result->numRows() == 1) {
245 $data = $this->db->fetchAssoc($result);
246 $this->setId((int) $question_id);
247 $this->setObjId((int) $data["obj_fi"]);
248 $this->setTitle((string) $data["title"]);
249 $this->setComment((string) $data["description"]);
250 $this->setOriginalId((int) $data["original_id"]);
251 $this->setNrOfTries((int) $data['nr_of_tries']);
252 $this->setAuthor($data["author"]);
253 $this->setPoints((float) $data["points"]);
254 $this->setOwner((int) $data["owner"]);
255 $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc((string) $data["question_text"], 1));
256 $this->setThumbGeometry((int) $data["thumb_geometry"]);
257 $this->setShuffle($data["shuffle"] != '0');
258 $this->setShuffleMode((int) $data['shuffle']);
259 $this->setMatchingMode($data['matching_mode'] === null ? self::MATCHING_MODE_1_ON_1 : $data['matching_mode']);
260
261 try {
262 $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
265 }
266
267 try {
268 $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
270 }
271 }
272
273 $termids = [];
274 $result = $this->db->queryF(
275 "SELECT * FROM qpl_a_mterm WHERE question_fi = %s ORDER BY term_id ASC",
276 ['integer'],
277 [$question_id]
278 );
279 $this->terms = [];
280 if ($result->numRows() > 0) {
281 while ($data = $this->db->fetchAssoc($result)) {
282 $term = $this->createMatchingTerm($data['term'] ?? '', $data['picture'] ?? '', (int) $data['ident']);
283 $this->terms[] = $term;
284 $termids[$data['term_id']] = $term;
285 }
286 }
287
288 $definitionids = [];
289 $result = $this->db->queryF(
290 "SELECT * FROM qpl_a_mdef WHERE question_fi = %s ORDER BY def_id ASC",
291 ['integer'],
292 [$question_id]
293 );
294
295 $this->definitions = [];
296 if ($result->numRows() > 0) {
297 while ($data = $this->db->fetchAssoc($result)) {
298 $definition = $this->createMatchingDefinition($data['definition'] ?? '', $data['picture'] ?? '', (int) $data['ident']);
299 array_push($this->definitions, $definition);
300 $definitionids[$data['def_id']] = $definition;
301 }
302 }
303
304 $this->matchingpairs = [];
305 $result = $this->db->queryF(
306 "SELECT * FROM qpl_a_matching WHERE question_fi = %s ORDER BY answer_id",
307 ['integer'],
308 [$question_id]
309 );
310 if ($result->numRows() > 0) {
311 while ($data = $this->db->fetchAssoc($result)) {
312 $pair = $this->createMatchingPair(
313 $termids[$data['term_fi']],
314 $definitionids[$data['definition_fi']],
315 (float) $data['points']
316 );
317 array_push($this->matchingpairs, $pair);
318 }
319 }
320 parent::loadFromDb((int) $question_id);
321 }
322
324 \assQuestion $target
325 ): \assQuestion {
326 $target->cloneImages($this->getId(), $this->getObjId(), $target->getId(), $target->getObjId());
327 return $target;
328 }
329
330 private function cloneImages(
331 int $source_question_id,
332 int $source_parent_id,
333 int $target_question_id,
334 int $target_parent_id
335 ): void {
336 $image_source_path = $this->getImagePath($source_question_id, $source_parent_id);
337 $image_target_path = $this->getImagePath($target_question_id, $target_parent_id);
338
339 if (!file_exists($image_target_path)) {
340 ilFileUtils::makeDirParents($image_target_path);
341 } else {
342 $this->removeAllImageFiles($image_target_path);
343 }
344
345 foreach ($this->terms as $term) {
346 if ($term->getPicture() === '') {
347 continue;
348 }
349
350 $filename = $term->getPicture();
351 if (!file_exists($image_source_path . $filename)
352 || !copy($image_source_path . $filename, $image_target_path . $filename)) {
353 $this->log->root()->warning('matching question image could not be copied: '
354 . $image_source_path . $filename);
355 }
356 if (!file_exists($image_source_path . $this->getThumbPrefix() . $filename)
357 || !copy(
358 $image_source_path . $this->getThumbPrefix() . $filename,
359 $image_target_path . $this->getThumbPrefix() . $filename
360 )) {
361 $this->log->root()->warning('matching question image thumbnail could not be copied: '
362 . $image_source_path . $this->getThumbPrefix() . $filename);
363 }
364 }
365 foreach ($this->definitions as $definition) {
366 if ($definition->getPicture() === '') {
367 continue;
368 }
369 $filename = $definition->getPicture();
370
371 if (!file_exists($image_source_path . $filename)
372 || !copy($image_source_path . $filename, $image_target_path . $filename)) {
373 $this->log->root()->warning('matching question image could not be copied: '
374 . $image_source_path . $filename);
375 }
376
377 if (!file_exists($image_source_path . $this->getThumbPrefix() . $filename)
378 || !copy(
379 $image_source_path . $this->getThumbPrefix() . $filename,
380 $image_target_path . $this->getThumbPrefix() . $filename
381 )) {
382 $this->log->root()->warning('matching question image thumbnail could not be copied: '
383 . $image_source_path . $this->getThumbPrefix() . $filename);
384 }
385 }
386 }
387
398 public function insertMatchingPair($position, $term = null, $definition = null, $points = 0.0): void
399 {
400 $pair = $this->createMatchingPair($term, $definition, $points);
401
402 if ($position < count($this->matchingpairs)) {
403 $part1 = array_slice($this->matchingpairs, 0, $position);
404 $part2 = array_slice($this->matchingpairs, $position);
405 $this->matchingpairs = array_merge($part1, [$pair], $part2);
406 } else {
407 array_push($this->matchingpairs, $pair);
408 }
409 }
410
422 public function addMatchingPair(?assAnswerMatchingTerm $term = null, ?assAnswerMatchingDefinition $definition = null, $points = 0.0): void
423 {
424 $pair = $this->createMatchingPair($term, $definition, $points);
425 array_push($this->matchingpairs, $pair);
426 }
427
431 public function getTermWithIdentifier($a_identifier)
432 {
433 foreach ($this->terms as $term) {
434 if ($term->getIdentifier() == $a_identifier) {
435 return $term;
436 }
437 }
438 return null;
439 }
440
444 public function getDefinitionWithIdentifier($a_identifier)
445 {
446 foreach ($this->definitions as $definition) {
447 if ($definition->getIdentifier() == $a_identifier) {
448 return $definition;
449 }
450 }
451 return null;
452 }
453
462 public function getMatchingPair($index = 0): ?object
463 {
464 if ($index < 0) {
465 return null;
466 }
467 if (count($this->matchingpairs) < 1) {
468 return null;
469 }
470 if ($index >= count($this->matchingpairs)) {
471 return null;
472 }
473 return $this->matchingpairs[$index];
474 }
475
483 public function deleteMatchingPair($index = 0): void
484 {
485 if ($index < 0) {
486 return;
487 }
488 if (count($this->matchingpairs) < 1) {
489 return;
490 }
491 if ($index >= count($this->matchingpairs)) {
492 return;
493 }
494 unset($this->matchingpairs[$index]);
495 $this->matchingpairs = array_values($this->matchingpairs);
496 }
497
502 public function flushMatchingPairs(): void
503 {
504 $this->matchingpairs = [];
505 }
506
510 public function withMatchingPairs(array $pairs): self
511 {
512 $clone = clone $this;
513 $clone->matchingpairs = $pairs;
514 return $clone;
515 }
516
517
524 public function getMatchingPairCount(): int
525 {
526 return count($this->matchingpairs);
527 }
528
535 public function getTerms(): array
536 {
537 return $this->terms;
538 }
539
546 public function getDefinitions(): array
547 {
548 return $this->definitions;
549 }
550
557 public function getTermCount(): int
558 {
559 return count($this->terms);
560 }
561
568 public function getDefinitionCount(): int
569 {
570 return count($this->definitions);
571 }
572
573 public function addTerm(assAnswerMatchingTerm $term): void
574 {
575 $this->terms[] = $term;
576 }
577
584 public function addDefinition($definition): void
585 {
586 array_push($this->definitions, $definition);
587 }
588
595 public function insertTerm($position, ?assAnswerMatchingTerm $term = null): void
596 {
597 if (is_null($term)) {
598 $term = $this->createMatchingTerm();
599 }
600 if ($position < count($this->terms)) {
601 $part1 = array_slice($this->terms, 0, $position);
602 $part2 = array_slice($this->terms, $position);
603 $this->terms = array_merge($part1, [$term], $part2);
604 } else {
605 array_push($this->terms, $term);
606 }
607 }
608
615 public function insertDefinition($position, ?assAnswerMatchingDefinition $definition = null): void
616 {
617 if (is_null($definition)) {
618 $definition = $this->createMatchingDefinition();
619 }
620 if ($position < count($this->definitions)) {
621 $part1 = array_slice($this->definitions, 0, $position);
622 $part2 = array_slice($this->definitions, $position);
623 $this->definitions = array_merge($part1, [$definition], $part2);
624 } else {
625 array_push($this->definitions, $definition);
626 }
627 }
628
633 public function flushTerms(): void
634 {
635 $this->terms = [];
636 }
637
642 public function flushDefinitions(): void
643 {
644 $this->definitions = [];
645 }
646
653 public function deleteTerm($position): void
654 {
655 unset($this->terms[$position]);
656 $this->terms = array_values($this->terms);
657 }
658
665 public function deleteDefinition($position): void
666 {
667 unset($this->definitions[$position]);
668 $this->definitions = array_values($this->definitions);
669 }
670
678 public function setTerm($term, $index): void
679 {
680 $this->terms[$index] = $term;
681 }
682
683 public function calculateReachedPoints(
684 int $active_id,
685 ?int $pass = null,
686 bool $authorized_solution = true
687 ): float {
688 $found_values = [];
689 if (is_null($pass)) {
690 $pass = $this->getSolutionMaxPass($active_id);
691 }
692 $result = $this->getCurrentSolutionResultSet($active_id, (int) $pass, $authorized_solution);
693 while ($data = $this->db->fetchAssoc($result)) {
694 if ($data['value1'] === '') {
695 continue;
696 }
697
698 if (!isset($found_values[$data['value2']])) {
699 $found_values[$data['value2']] = [];
700 }
701
702 $found_values[$data['value2']][] = $data['value1'];
703 }
704
705 $points = $this->calculateReachedPointsForSolution($found_values);
706
707 return $points;
708 }
709
713 public function getMaximumPoints(): float
714 {
715 $points = 0;
716
717 foreach ($this->getMaximumScoringMatchingPairs() as $pair) {
718 $points += $pair->getPoints();
719 }
720
721 return $points;
722 }
723
724 public function getMaximumScoringMatchingPairs(): array
725 {
726 if ($this->getMatchingMode() == self::MATCHING_MODE_N_ON_N) {
727 return $this->getPositiveScoredMatchingPairs();
728 } elseif ($this->getMatchingMode() == self::MATCHING_MODE_1_ON_1) {
729 return $this->getMostPositiveScoredUniqueTermMatchingPairs();
730 }
731
732 return [];
733 }
734
735 private function getPositiveScoredMatchingPairs(): array
736 {
737 $matchingPairs = [];
738
739 foreach ($this->matchingpairs as $pair) {
740 if ($pair->getPoints() <= 0) {
741 continue;
742 }
743
744 $matchingPairs[] = $pair;
745 }
746
747 return $matchingPairs;
748 }
749
751 {
752 $matchingPairsByDefinition = [];
753
754 foreach ($this->matchingpairs as $pair) {
755 if ($pair->getPoints() <= 0) {
756 continue;
757 }
758
759 $defId = $pair->getDefinition()->getIdentifier();
760
761 if (!isset($matchingPairsByDefinition[$defId])) {
762 $matchingPairsByDefinition[$defId] = $pair;
763 } elseif ($pair->getPoints() > $matchingPairsByDefinition[$defId]->getPoints()) {
764 $matchingPairsByDefinition[$defId] = $pair;
765 }
766 }
767
768 return $matchingPairsByDefinition;
769 }
770
775 public function fetchIndexedValuesFromValuePairs(array $valuePairs): array
776 {
777 $indexedValues = [];
778
779 foreach ($valuePairs as $valuePair) {
780 if (!isset($indexedValues[$valuePair['value2']])) {
781 $indexedValues[$valuePair['value2']] = [];
782 }
783
784 $indexedValues[$valuePair['value2']][] = $valuePair['value1'];
785 }
786
787 return $indexedValues;
788 }
789
798 public function getEncryptedFilename($filename): string
799 {
800 $extension = "";
801 if (preg_match("/.*\\.(\\w+)$/", $filename, $matches)) {
802 $extension = $matches[1];
803 }
804 return md5($filename) . "." . $extension;
805 }
806
807 public function removeTermImage($index): void
808 {
809 $term = $this->terms[$index] ?? null;
810 if (is_object($term)) {
811 $this->deleteImagefile($term->getPicture());
812 $term = $term->withPicture('');
813 }
814 }
815
816 public function removeDefinitionImage($index): void
817 {
818 $definition = $this->definitions[$index] ?? null;
819 if (is_object($definition)) {
820 $this->deleteImagefile($definition->getPicture());
821 $definition = $definition->withPicture('');
822 }
823 }
824
825
832 public function deleteImagefile(string $filename): bool
833 {
834 $deletename = $filename;
835 try {
836 $result = unlink($this->getImagePath() . $deletename)
837 && unlink($this->getImagePath() . $this->getThumbPrefix() . $deletename);
838 } catch (Throwable $e) {
839 $result = false;
840 }
841 return $result;
842 }
843
844 public function setImageFile(
845 string $image_tempfilename,
846 string $image_filename,
847 string $previous_filename = ''
848 ): bool {
849 $result = true;
850 if ($image_tempfilename === '') {
851 return true;
852 }
853
854 $image_filename = str_replace(' ', '_', $image_filename);
855 $imagepath = $this->getImagePath();
856 if (!file_exists($imagepath)) {
857 ilFileUtils::makeDirParents($imagepath);
858 }
859
861 $image_tempfilename,
862 $image_filename,
863 $imagepath . $image_filename
864 )
865 ) {
866 return false;
867 }
868
869 // create thumbnail file
870 $thumbpath = $imagepath . $this->getThumbPrefix() . $image_filename;
872 $imagepath . $image_filename,
873 $thumbpath,
874 'JPEG',
875 (string) $this->getThumbGeometry()
876 );
877
878 if ($result
879 && $image_filename !== $previous_filename
880 && $previous_filename !== ''
881 ) {
882 $this->deleteImagefile($previous_filename);
883 }
884 return $result;
885 }
886
887 private function checkSubmittedMatchings(array $submitted_matchings): bool
888 {
889 if ($this->getMatchingMode() == self::MATCHING_MODE_N_ON_N) {
890 return true;
891 }
892
893 $handledTerms = [];
894
895 foreach ($submitted_matchings as $terms) {
896 if (count($terms) > 1) {
897 $this->tpl->setOnScreenMessage('failure', $this->lng->txt("multiple_matching_values_selected"), true);
898 return false;
899 }
900
901 foreach ($terms as $i => $term) {
902 if (isset($handledTerms[$term])) {
903 $this->tpl->setOnScreenMessage('failure', $this->lng->txt("duplicate_matching_values_selected"), true);
904 return false;
905 }
906
907 $handledTerms[$term] = $term;
908 }
909 }
910
911 return true;
912 }
913
914 public function saveWorkingData(
915 int $active_id,
916 ?int $pass = null,
917 bool $authorized = true
918 ): bool {
919 if ($pass === null) {
920 $pass = ilObjTest::_getPass($active_id);
921 }
922
923 $submitted_matchings = $this->questionpool_request->getMatchingPairs();
924 if (!$this->checkSubmittedMatchings($submitted_matchings)) {
925 return false;
926 }
927
928 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
929 function () use ($submitted_matchings, $active_id, $pass, $authorized) {
930 $this->removeCurrentSolution($active_id, $pass, $authorized);
931 foreach ($submitted_matchings as $definition => $terms) {
932 foreach ($terms as $i => $term) {
933 $this->saveCurrentSolution($active_id, $pass, $term, $definition, $authorized);
934 }
935 }
936 }
937 );
938
939 return true;
940 }
941
942 protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
943 {
944 $submitted_matchings = $this->questionpool_request->getMatchingPairs();
945
946 if ($this->checkSubmittedMatchings($submitted_matchings)) {
947 $previewSession->setParticipantsSolution($submitted_matchings);
948 }
949 }
950
951 public function getRandomId(): int
952 {
953 mt_srand((float) microtime() * 1000000);
954 $random_number = mt_rand(1, 100000);
955 $found = false;
956 while ($found) {
957 $found = false;
958 foreach ($this->matchingpairs as $key => $pair) {
959 if (($pair->getTerm()->getIdentifier() == $random_number) || ($pair->getDefinition()->getIdentifier() == $random_number)) {
960 $found = true;
961 $random_number++;
962 }
963 }
964 }
965 return $random_number;
966 }
967
968 public function setShuffle($shuffle = true): void
969 {
970 $this->shuffle = (bool) $shuffle;
971 }
972
978 public function getQuestionType(): string
979 {
980 return "assMatchingQuestion";
981 }
982
983 public function getAdditionalTableName(): string
984 {
985 return "qpl_qst_matching";
986 }
987
988 public function getAnswerTableName(): array
989 {
990 return ["qpl_a_matching", "qpl_a_mterm"];
991 }
992
997 public function getRTETextWithMediaObjects(): string
998 {
999 return parent::getRTETextWithMediaObjects();
1000 }
1001
1005 public function getMatchingPairs(): array
1006 {
1007 return $this->matchingpairs;
1008 }
1009
1015 public function getThumbGeometry(): int
1016 {
1017 return $this->thumb_geometry;
1018 }
1019
1025 public function getThumbSize(): int
1026 {
1027 return $this->getThumbGeometry();
1028 }
1029
1035 public function setThumbGeometry(int $a_geometry): void
1036 {
1037 $this->thumb_geometry = ($a_geometry < 1) ? 100 : $a_geometry;
1038 }
1039
1043 public function rebuildThumbnails(): void
1044 {
1045 $new_terms = [];
1046 foreach ($this->terms as $term) {
1047 if ($term->getPicture() !== '') {
1048 $current_file_path = $this->getImagePath() . $term->getPicture();
1049 if (!file_exists($current_file_path)) {
1050 $new_terms[] = $term;
1051 continue;
1052 }
1053 $new_file_name = $this->buildHashedImageFilename($term->getPicture(), true);
1054 $new_file_path = $this->getImagePath() . $new_file_name;
1055 rename($current_file_path, $new_file_path);
1056 $term = $term->withPicture($new_file_name);
1057 $this->generateThumbForFile($this->getImagePath(), $term->getPicture());
1058 }
1059 $new_terms[] = $term;
1060 }
1061 $this->terms = $new_terms;
1062
1063 $new_definitions = [];
1064 foreach ($this->definitions as $definition) {
1065 if ($definition->getPicture() !== '') {
1066 $current_file_path = $this->getImagePath() . $definition->getPicture();
1067 if (!file_exists($current_file_path)) {
1068 $new_definitions[] = $definition;
1069 continue;
1070 }
1071 $new_file_name = $this->buildHashedImageFilename($definition->getPicture(), true);
1072 $new_file_path = $this->getImagePath() . $new_file_name;
1073 rename($current_file_path, $new_file_path);
1074 $definition = $definition->withPicture($new_file_name);
1075 $this->generateThumbForFile($this->getImagePath(), $definition->getPicture());
1076 }
1077 $new_definitions[] = $definition;
1078 }
1079 $this->definitions = $new_definitions;
1080 }
1081
1082 public function getThumbPrefix(): string
1083 {
1084 return "thumb.";
1085 }
1086
1087 protected function generateThumbForFile($path, $file): void
1088 {
1089 $filename = $path . $file;
1090 if (file_exists($filename)) {
1091 $thumbpath = $path . $this->getThumbPrefix() . $file;
1092 $path_info = pathinfo($filename);
1093 $ext = "";
1094 switch (strtoupper($path_info['extension'])) {
1095 case 'PNG':
1096 $ext = 'PNG';
1097 break;
1098 case 'GIF':
1099 $ext = 'GIF';
1100 break;
1101 default:
1102 $ext = 'JPEG';
1103 break;
1104 }
1105 ilShellUtil::convertImage($filename, $thumbpath, $ext, (string) $this->getThumbGeometry());
1106 }
1107 }
1108
1112 public function toJSON(): string
1113 {
1114 $result = [];
1115
1116 $result['id'] = $this->getId();
1117 $result['type'] = (string) $this->getQuestionType();
1118 $result['title'] = $this->getTitleForHTMLOutput();
1119 $result['question'] = $this->formatSAQuestion($this->getQuestion());
1120 $result['nr_of_tries'] = $this->getNrOfTries();
1121 $result['matching_mode'] = $this->getMatchingMode();
1122 $result['shuffle'] = true;
1123 $result['feedback'] = [
1124 'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1125 'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1126 ];
1127
1128 $this->setShuffler($this->randomGroup->shuffleArray(new RandomSeed()));
1129
1130 $terms = [];
1131 foreach ($this->getShuffler()->transform($this->getTerms()) as $term) {
1132 $terms[] = [
1133 "text" => $this->formatSAQuestion($term->getText()),
1134 "id" => $this->getId() . $term->getIdentifier()
1135 ];
1136 }
1137 $result['terms'] = $terms;
1138
1139 $this->setShuffler($this->randomGroup->shuffleArray(new RandomSeed()));
1140
1141 $definitions = [];
1142 foreach ($this->getShuffler()->transform($this->getDefinitions()) as $def) {
1143 $definitions[] = [
1144 "text" => $this->formatSAQuestion((string) $def->getText()),
1145 "id" => $this->getId() . $def->getIdentifier()
1146 ];
1147 }
1148 $result['definitions'] = $definitions;
1149
1150 // #10353
1151 $matchings = [];
1152 foreach ($this->getMatchingPairs() as $pair) {
1153 // fau: fixLmMatchingPoints - ignore matching pairs with 0 or negative points
1154 if ($pair->getPoints() <= 0) {
1155 continue;
1156 }
1157 // fau.
1158
1159 $pid = $pair->getDefinition()->getIdentifier();
1160 if ($this->getMatchingMode() == self::MATCHING_MODE_N_ON_N) {
1161 $pid .= '::' . $pair->getTerm()->getIdentifier();
1162 }
1163
1164 if (!isset($matchings[$pid]) || $matchings[$pid]["points"] < $pair->getPoints()) {
1165 $matchings[$pid] = [
1166 "term_id" => $this->getId() . $pair->getTerm()->getIdentifier(),
1167 "def_id" => $this->getId() . $pair->getDefinition()->getIdentifier(),
1168 "points" => (int) $pair->getPoints()
1169 ];
1170 }
1171 }
1172
1173 $result['matchingPairs'] = array_values($matchings);
1174
1175 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1176 $result['mobs'] = $mobs;
1177
1178 $this->lng->loadLanguageModule('assessment');
1179 $result['reset_button_label'] = $this->lng->txt("reset_terms");
1180
1181 return json_encode($result);
1182 }
1183
1184 public function setMatchingMode(string $matching_mode): void
1185 {
1186 $this->matching_mode = $matching_mode;
1187 }
1188
1189 public function getMatchingMode(): string
1190 {
1191 return $this->matching_mode;
1192 }
1193
1194 protected function calculateReachedPointsForSolution(?array $found_values): float
1195 {
1196 $points = 0.0;
1197 if (!is_array($found_values)) {
1198 return $points;
1199 }
1200 foreach ($found_values as $definition => $terms) {
1201 if (!is_array($terms)) {
1202 continue;
1203 }
1204 foreach ($terms as $term) {
1205 foreach ($this->matchingpairs as $pair) {
1206 if ($pair->getDefinition()->getIdentifier() == $definition
1207 && $pair->getTerm()->getIdentifier() == $term) {
1208 $points += $pair->getPoints();
1209 }
1210 }
1211 }
1212 }
1213 return $points;
1214 }
1215
1216 public function getOperators(string $expression): array
1217 {
1219 }
1220
1221 public function getExpressionTypes(): array
1222 {
1223 return [
1228 ];
1229 }
1230
1231 public function getUserQuestionResult(
1232 int $active_id,
1233 int $pass
1235 $result = new ilUserQuestionResult($this, $active_id, $pass);
1236
1237 $data = $this->db->queryF(
1238 "SELECT ident FROM qpl_a_mdef WHERE question_fi = %s ORDER BY def_id",
1239 ["integer"],
1240 [$this->getId()]
1241 );
1242
1243 $definitions = [];
1244 for ($index = 1; $index <= $this->db->numRows($data); ++$index) {
1245 $row = $this->db->fetchAssoc($data);
1246 $definitions[$row["ident"]] = $index;
1247 }
1248
1249 $data = $this->db->queryF(
1250 "SELECT ident FROM qpl_a_mterm WHERE question_fi = %s ORDER BY term_id",
1251 ["integer"],
1252 [$this->getId()]
1253 );
1254
1255 $terms = [];
1256 for ($index = 1; $index <= $this->db->numRows($data); ++$index) {
1257 $row = $this->db->fetchAssoc($data);
1258 $terms[$row["ident"]] = $index;
1259 }
1260
1261 $maxStep = $this->lookupMaxStep($active_id, $pass);
1262
1263 if ($maxStep > 0) {
1264 $data = $this->db->queryF(
1265 "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s",
1266 ["integer", "integer", "integer","integer"],
1267 [$active_id, $pass, $this->getId(), $maxStep]
1268 );
1269 } else {
1270 $data = $this->db->queryF(
1271 "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s",
1272 ["integer", "integer", "integer"],
1273 [$active_id, $pass, $this->getId()]
1274 );
1275 }
1276
1277 while ($row = $this->db->fetchAssoc($data)) {
1278 if ($row["value1"] > 0) {
1279 $result->addKeyValue($definitions[$row["value2"]], $terms[$row["value1"]]);
1280 }
1281 }
1282
1283 $points = $this->calculateReachedPoints($active_id, $pass);
1284 $max_points = $this->getMaximumPoints();
1285
1286 $result->setReachedPercentage(($points / $max_points) * 100);
1287
1288 return $result;
1289 }
1290
1297 public function getAvailableAnswerOptions($index = null)
1298 {
1299 if ($index !== null) {
1300 return $this->getMatchingPair($index);
1301 } else {
1302 return $this->getMatchingPairs();
1303 }
1304 }
1305
1309 protected function afterSyncWithOriginal(
1310 int $original_question_id,
1311 int $clone_question_id,
1312 int $original_parent_id,
1313 int $clone_parent_id
1314 ): void {
1315 parent::afterSyncWithOriginal($original_question_id, $clone_question_id, $original_parent_id, $clone_parent_id);
1316
1317 $original_image_path = $this->question_files->buildImagePath($original_question_id, $original_parent_id);
1318 $clone_image_path = $this->question_files->buildImagePath($clone_question_id, $clone_parent_id);
1319
1320 ilFileUtils::delDir($original_image_path);
1321 if (is_dir($clone_image_path)) {
1322 ilFileUtils::makeDirParents($original_image_path);
1323 ilFileUtils::rCopy($clone_image_path, $original_image_path);
1324 }
1325 }
1326
1327 protected function createMatchingTerm(string $term = '', string $picture = '', int $identifier = 0): assAnswerMatchingTerm
1328 {
1329 return new assAnswerMatchingTerm($term, $picture, $identifier);
1330 }
1331 protected function createMatchingDefinition(string $term = '', string $picture = '', int $identifier = 0): assAnswerMatchingDefinition
1332 {
1333 return new assAnswerMatchingDefinition($term, $picture, $identifier);
1334 }
1335 protected function createMatchingPair(
1336 ?assAnswerMatchingTerm $term = null,
1337 ?assAnswerMatchingDefinition $definition = null,
1338 float $points = 0.0
1340 $term = $term ?? $this->createMatchingTerm();
1341 $definition = $definition ?? $this->createMatchingDefinition();
1342 return new assAnswerMatchingPair($term, $definition, $points);
1343 }
1344
1345 public function toLog(AdditionalInformationGenerator $additional_info): array
1346 {
1347 $result = [
1348 AdditionalInformationGenerator::KEY_QUESTION_TYPE => (string) $this->getQuestionType(),
1349 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $this->getTitleForHTMLOutput(),
1350 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $this->formatSAQuestion($this->getQuestion()),
1351 AdditionalInformationGenerator::KEY_QUESTION_SHUFFLE_ANSWER_OPTIONS => $additional_info
1352 ->getTrueFalseTagForBool($this->getShuffle()),
1353 'qpl_qst_inp_matching_mode' => $this->getMatchingMode() === self::MATCHING_MODE_1_ON_1 ? '{{ qpl_qst_inp_matching_mode_one_on_one }}' : '{{ qpl_qst_inp_matching_mode_all_on_all }}',
1354 AdditionalInformationGenerator::KEY_FEEDBACK => [
1355 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_INCOMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1356 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_COMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1357 ]
1358 ];
1359
1360 foreach ($this->getTerms() as $term) {
1361 $result[AdditionalInformationGenerator::KEY_QUESTION_MATCHING_TERMS][] = $term->getText();
1362 }
1363
1364 foreach ($this->getDefinitions() as $definition) {
1365 $result[AdditionalInformationGenerator::KEY_QUESTION_MATCHING_DEFINITIONS][] = $this->formatSAQuestion((string) $definition->getText());
1366 }
1367
1368 // #10353
1369 $matching_pairs = [];
1370 $i = 1;
1371 foreach ($this->getMatchingPairs() as $pair) {
1372 $matching_pairs[$i++] = [
1373 AdditionalInformationGenerator::KEY_QUESTION_MATCHING_TERM => $pair->getTerm()->getText(),
1374 AdditionalInformationGenerator::KEY_QUESTION_MATCHING_DEFINITION => $this->formatSAQuestion((string) $pair->getDefinition()->getText()),
1375 AdditionalInformationGenerator::KEY_QUESTION_REACHABLE_POINTS => (int) $pair->getPoints()
1376 ];
1377 }
1378
1379 $result[AdditionalInformationGenerator::KEY_QUESTION_CORRECT_ANSWER_OPTIONS] = $matching_pairs;
1380 return $result;
1381 }
1382
1383 protected function solutionValuesToLog(
1384 AdditionalInformationGenerator $additional_info,
1385 array $solution_values
1386 ): array {
1387 return $this->solutionValuesToText($solution_values);
1388 }
1389
1390 public function solutionValuesToText(array $solution_values): array
1391 {
1392 $reducer = static function (array $c, assAnswerMatchingTerm|assAnswerMatchingDefinition $v): array {
1393 $c[$v->getIdentifier()] = $v->getText() !== ''
1394 ? $v->getPicture()
1395 : $v->getText();
1396 return $c;
1397 };
1398
1399 $terms_by_identifier = array_reduce(
1400 $this->getTerms(),
1401 $reducer,
1402 []
1403 );
1404
1405 $definitions_by_identifier = array_reduce(
1406 $this->getDefinitions(),
1407 $reducer,
1408 []
1409 );
1410
1411 return array_map(
1412 static fn(array $v): string => $definitions_by_identifier[$v['value2']]
1413 . ':' . $terms_by_identifier[$v['value1']],
1414 $solution_values
1415 );
1416 }
1417
1418 public function getCorrectSolutionForTextOutput(int $active_id, int $pass): array
1419 {
1420 return array_map(
1421 fn(assAnswerMatchingPair $v): string => $v->getDefinition()->getText() . ': '
1422 . $v->getTerm()->getText(),
1423 $this->getMatchingPairs()
1424 );
1425 }
1426}
$filename
Definition: buildRTE.php:78
return true
Class for matching question definitions.
Class for matching question pairs.
Class for matching question terms.
Class for matching questions.
afterSyncWithOriginal(int $original_question_id, int $clone_question_id, int $original_parent_id, int $clone_parent_id)
{}
deleteDefinition($position)
Deletes a definition.
getCorrectSolutionForTextOutput(int $active_id, int $pass)
getEncryptedFilename($filename)
Returns the encrypted save filename of a matching picture Images are saved with an encrypted filename...
getMatchingPairCount()
Returns the number of matching pairs.
addDefinition($definition)
Adds a definition.
getMaximumPoints()
Calculates and Returns the maximum points, a learner can reach answering the question.
setThumbGeometry(int $a_geometry)
Set the thumbnail geometry.
savePreviewData(ilAssQuestionPreviewSession $previewSession)
toJSON()
Returns a JSON representation of the question.
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.
getDefinitionCount()
Returns the number of definitions.
getDefinitions()
Returns the definitions of the matching question.
getMatchingPair($index=0)
Returns a matching pair with a given index.
createMatchingDefinition(string $term='', string $picture='', int $identifier=0)
getQuestionType()
Returns the question type of the question.
rebuildThumbnails()
Rebuild the thumbnail images with a new thumbnail size.
addMatchingPair(?assAnswerMatchingTerm $term=null, ?assAnswerMatchingDefinition $definition=null, $points=0.0)
Adds an matching pair for an matching choice question.
getMatchingPairs()
Returns the matchingpairs array.
getTermCount()
Returns the number of terms.
toLog(AdditionalInformationGenerator $additional_info)
MUST return an array of the question settings that can be stored in the log.
getThumbGeometry()
Get the thumbnail geometry.
deleteTerm($position)
Deletes a term.
cloneImages(int $source_question_id, int $source_parent_id, int $target_question_id, int $target_parent_id)
createMatchingTerm(string $term='', string $picture='', int $identifier=0)
getTerms()
Returns the terms of the matching question.
createMatchingPair(?assAnswerMatchingTerm $term=null, ?assAnswerMatchingDefinition $definition=null, float $points=0.0)
insertDefinition($position, ?assAnswerMatchingDefinition $definition=null)
Inserts a definition.
calculateReachedPoints(int $active_id, ?int $pass=null, bool $authorized_solution=true)
flushDefinitions()
Deletes all definitions.
fetchIndexedValuesFromValuePairs(array $valuePairs)
loadFromDb($question_id)
Loads a assMatchingQuestion object from a database.
getDefinitionWithIdentifier($a_identifier)
Returns a definition with a given identifier.
solutionValuesToText(array $solution_values)
MUST convert the given solution values into text.
calculateReachedPointsForSolution(?array $found_values)
flushTerms()
Deletes all terms.
flushMatchingPairs()
Deletes all matching pairs.
checkSubmittedMatchings(array $submitted_matchings)
addTerm(assAnswerMatchingTerm $term)
getOperators(string $expression)
Get all available operations for a specific question.
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
getThumbSize()
Get the thumbnail geometry.
getTermWithIdentifier($a_identifier)
Returns a term with a given identifier.
setTerm($term, $index)
Sets a specific term.
saveWorkingData(int $active_id, ?int $pass=null, bool $authorized=true)
cloneQuestionTypeSpecificProperties(\assQuestion $target)
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
__construct( $title="", $comment="", $author="", $owner=-1, $question="", $matching_type=self::MT_TERMS_DEFINITIONS)
assMatchingQuestion constructor
getUserQuestionResult(int $active_id, int $pass)
Get the user solution for a question by active_id and the test pass.
setImageFile(string $image_tempfilename, string $image_filename, string $previous_filename='')
deleteImagefile(string $filename)
Deletes an imagefile from the system if the file is deleted manually.
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
saveToDb(?int $original_id=null)
getExpressionTypes()
Get all available expression types for a specific question.
insertTerm($position, ?assAnswerMatchingTerm $term=null)
Inserts a term.
setMatchingMode(string $matching_mode)
deleteMatchingPair($index=0)
Deletes a matching pair with a given index.
insertMatchingPair($position, $term=null, $definition=null, $points=0.0)
Inserts a matching pair for an matching choice question.
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
setOriginalId(?int $original_id)
setId(int $id=-1)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
setQuestion(string $question="")
setAuthor(string $author="")
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)
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
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
static rCopy(string $a_sdir, string $a_tdir, bool $preserveTimeAttributes=false)
Copies content of a directory $a_sdir recursively to a directory $a_tdir.
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="")
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$c
Definition: deliver.php:25
return['delivery_method'=> 'php',]
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
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
if(!file_exists('../ilias.ini.php'))
global $DIC
Definition: shib_login.php:26