ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
class.assMatchingQuestion.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
24 use 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;
52  protected string $matching_mode = self::MATCHING_MODE_1_ON_1;
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 
137  public function saveAnswerSpecificDataToDb()
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  );
187  $matchingpairs = $this->getMatchingPairs();
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']);
269  } catch (ilTestQuestionPoolException $e) {
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) {
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  {
1218  return ilOperatorsExpressionMapping::getOperatorsByExpression($expression);
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 }
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...
getThumbGeometry()
Get the thumbnail geometry.
Class for matching question terms.
setNrOfTries(int $a_nr_of_tries)
addTerm(assAnswerMatchingTerm $term)
getMaximumPoints()
Calculates and Returns the maximum points, a learner can reach answering the question.
insertTerm($position, ?assAnswerMatchingTerm $term=null)
Inserts a term.
Class for matching question pairs.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
getMatchingPairs()
Returns the matchingpairs array.
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
getTermCount()
Returns the number of terms.
calculateReachedPoints(int $active_id, ?int $pass=null, bool $authorized_solution=true)
calculateReachedPointsForSolution(?array $found_values)
deleteMatchingPair($index=0)
Deletes a matching pair with a given index.
removeAllImageFiles(string $image_target_path)
setOwner(int $owner=-1)
getDefinitionCount()
Returns the number of definitions.
addMatchingPair(?assAnswerMatchingTerm $term=null, ?assAnswerMatchingDefinition $definition=null, $points=0.0)
Adds an matching pair for an matching choice question.
getQuestionType()
Returns the question type of the question.
flushMatchingPairs()
Deletes all matching pairs.
getEncryptedFilename($filename)
Returns the encrypted save filename of a matching picture Images are saved with an encrypted filename...
cloneImages(int $source_question_id, int $source_parent_id, int $target_question_id, int $target_parent_id)
flushTerms()
Deletes all terms.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static rCopy(string $a_sdir, string $a_tdir, bool $preserveTimeAttributes=false)
Copies content of a directory $a_sdir recursively to a directory $a_tdir.
fetchIndexedValuesFromValuePairs(array $valuePairs)
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
$c
Definition: deliver.php:25
deleteTerm($position)
Deletes a term.
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
setComment(string $comment="")
setThumbGeometry(int $a_geometry)
Set the thumbnail geometry.
rebuildThumbnails()
Rebuild the thumbnail images with a new thumbnail size.
getDefinitions()
Returns the definitions of the matching question.
loadFromDb($question_id)
Loads a assMatchingQuestion object from a database.
getMatchingPair($index=0)
Returns a matching pair with a given index.
$path
Definition: ltiservices.php:29
Class for matching questions.
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
__construct( $title="", $comment="", $author="", $owner=-1, $question="", $matching_type=self::MT_TERMS_DEFINITIONS)
assMatchingQuestion constructor
saveToDb(?int $original_id=null)
getTermWithIdentifier($a_identifier)
Returns a term with a given identifier.
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
insertMatchingPair($position, $term=null, $definition=null, $points=0.0)
Inserts a matching pair for an matching choice question.
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
getOperators(string $expression)
Get all available operations for a specific question.
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
buildHashedImageFilename(string $plain_image_filename, bool $unique=false)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setTerm($term, $index)
Sets a specific term.
getMatchingPairCount()
Returns the number of matching pairs.
getExpressionTypes()
Get all available expression types for a specific question.
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
cloneQuestionTypeSpecificProperties(\assQuestion $target)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
global $DIC
Definition: shib_login.php:26
deleteImagefile(string $filename)
Deletes an imagefile from the system if the file is deleted manually.
addDefinition($definition)
Adds a definition.
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
setPoints(float $points)
setObjId(int $obj_id=0)
saveQuestionDataToDb(?int $original_id=null)
static convertImage(string $a_from, string $a_to, string $a_target_format="", string $a_geometry="", string $a_background_color="")
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
toLog(AdditionalInformationGenerator $additional_info)
flushDefinitions()
Deletes all definitions.
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
$filename
Definition: buildRTE.php:78
createMatchingTerm(string $term='', string $picture='', int $identifier=0)
toJSON()
Returns a JSON representation of the question.
deleteDefinition($position)
Deletes a definition.
getUserQuestionResult(int $active_id, int $pass)
Get the user solution for a question by active_id and the test pass.
createMatchingPair(?assAnswerMatchingTerm $term=null, ?assAnswerMatchingDefinition $definition=null, float $points=0.0)
getSolutionMaxPass(int $active_id)
afterSyncWithOriginal(int $original_question_id, int $clone_question_id, int $original_parent_id, int $clone_parent_id)
{}
getTerms()
Returns the terms of the matching question.
solutionValuesToText(array $solution_values)
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
getDefinitionWithIdentifier($a_identifier)
Returns a definition with a given identifier.
setMatchingMode(string $matching_mode)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setId(int $id=-1)
__construct(Container $dic, ilPlugin $plugin)
insertDefinition($position, ?assAnswerMatchingDefinition $definition=null)
Inserts a definition.
setOriginalId(?int $original_id)
setShuffler(Transformation $shuffler)
setTitle(string $title="")
createMatchingDefinition(string $term='', string $picture='', int $identifier=0)
Class for matching question definitions.
setLifecycle(ilAssQuestionLifecycle $lifecycle)
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
getCorrectSolutionForTextOutput(int $active_id, int $pass)
setImageFile(string $image_tempfilename, string $image_filename, string $previous_filename='')
lookupMaxStep(int $active_id, int $pass)
setAuthor(string $author="")
savePreviewData(ilAssQuestionPreviewSession $previewSession)
solutionValuesToLog(AdditionalInformationGenerator $additional_info, array $solution_values)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
saveWorkingData(int $active_id, ?int $pass=null, bool $authorized=true)
checkSubmittedMatchings(array $submitted_matchings)
getThumbSize()
Get the thumbnail geometry.
setQuestion(string $question="")