ILIAS  release_10 Revision v10.1-43-ga1241a92c2f
class.assOrderingQuestion.php
Go to the documentation of this file.
1 <?php
2 
19 declare(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;
59  public $old_ordering_depth = [];
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  }
103  return $this->oq_repository;
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']);
156  } catch (ilTestQuestionPoolException $e) {
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->deductHintPointsFromReachedPoints(
506  $preview_session,
507  $this->calculateReachedPointsForSolution($solution_ordering_element_list)
508  );
509 
510  return $this->ensureNonNegativePoints($reached_points);
511  }
512 
513  public function getMaximumPoints(): float
514  {
515  return $this->getPoints();
516  }
517 
518  /*
519  * Returns the encrypted save filename of a matching picture
520  * Images are saved with an encrypted filename to prevent users from
521  * cheating by guessing the solution from the image filename
522  *
523  * @param string $filename Original filename
524  * @return string Encrypted filename
525  */
526  public function getEncryptedFilename($filename): string
527  {
528  $extension = "";
529  if (preg_match("/.*\\.(\\w+)$/", $filename, $matches)) {
530  $extension = $matches[1];
531  }
532  return md5($filename) . "." . $extension;
533  }
534 
535  protected function cleanImagefiles(): void
536  {
537  if ($this->getOrderingType() == self::OQ_PICTURES) {
538  if (@file_exists($this->getImagePath())) {
539  $contents = ilFileUtils::getDir($this->getImagePath());
540  foreach ($contents as $f) {
541  if (strcmp($f['type'], 'file') == 0) {
542  $found = false;
543  foreach ($this->getOrderingElementList() as $orderElement) {
544  if (strcmp($f['entry'], $orderElement->getContent()) == 0) {
545  $found = true;
546  }
547  if (strcmp($f['entry'], $this->getThumbPrefix() . $orderElement->getContent()) == 0) {
548  $found = true;
549  }
550  }
551  if (!$found) {
552  if (@file_exists($this->getImagePath() . $f['entry'])) {
553  @unlink($this->getImagePath() . $f['entry']);
554  }
555  }
556  }
557  }
558  }
559  } else {
560  if (@file_exists($this->getImagePath())) {
562  }
563  }
564  }
565 
566  /*
567  * Deletes an imagefile from the system if the file is deleted manually
568  *
569  * @param string $filename Image file filename
570  * @return boolean Success
571  */
572  public function dropImageFile($imageFilename)
573  {
574  if (!strlen($imageFilename)) {
575  return false;
576  }
577 
578  $result = @unlink($this->getImagePath() . $imageFilename);
579  $result = $result && @unlink($this->getImagePath() . $this->getThumbPrefix() . $imageFilename);
580 
581  return $result;
582  }
583 
584  public function isImageFileStored($imageFilename): bool
585  {
586  if (!strlen($imageFilename)) {
587  return false;
588  }
589 
590  if (!file_exists($this->getImagePath() . $imageFilename)) {
591  return false;
592  }
593 
594  return is_file($this->getImagePath() . $imageFilename);
595  }
596 
597  public function isImageReplaced(ilAssOrderingElement $newElement, ilAssOrderingElement $oldElement): bool
598  {
599  if (!$this->hasOrderingTypeUploadSupport()) {
600  return false;
601  }
602 
603  if (!$newElement->getContent()) {
604  return false;
605  }
606 
607  return $newElement->getContent() != $oldElement->getContent();
608  }
609 
610 
611  public function storeImageFile(string $upload_file, string $upload_name): ?string
612  {
613  $name_parts = explode(".", $upload_name);
614  $suffix = strtolower(array_pop($name_parts));
615  if (!in_array($suffix, self::VALID_UPLOAD_SUFFIXES)) {
616  return null;
617  }
618 
619  $this->ensureImagePathExists();
620  $target_filename = $this->buildHashedImageFilename($upload_name, true);
621  $target_filepath = $this->getImagePath() . $target_filename;
622  if (ilFileUtils::moveUploadedFile($upload_file, $target_filename, $target_filepath)) {
623  $thumb_path = $this->getImagePath() . $this->getThumbPrefix() . $target_filename;
624  ilShellUtil::convertImage($target_filepath, $thumb_path, "JPEG", (string) $this->getThumbSize());
625 
626  return $target_filename;
627  }
628 
629  return null;
630  }
631 
632  public function updateImageFile(string $existing_image_name): ?string
633  {
634  $existing_image_path = $this->getImagePath() . $existing_image_name;
635  $target_filename = $this->buildHashedImageFilename($existing_image_name, true);
636  $target_filepath = $this->getImagePath() . $target_filename;
637  if (ilFileUtils::rename($existing_image_path, $target_filepath)) {
638  unlink($this->getImagePath() . $this->getThumbPrefix() . $existing_image_name);
639  $thumb_path = $this->getImagePath() . $this->getThumbPrefix() . $target_filename;
640  ilShellUtil::convertImage($target_filepath, $thumb_path, "JPEG", (string) $this->getThumbSize());
641 
642  return $target_filename;
643  }
644 
645  return $existing_image_name;
646  }
647 
648  public function validateSolutionSubmit(): bool
649  {
650  $submittedSolutionList = $this->getSolutionListFromPostSubmit();
651 
652  if (!$submittedSolutionList->hasElements()) {
653  return true;
654  }
655 
656  return $this->getOrderingElementList()->hasSameElementSetByRandomIdentifiers($submittedSolutionList);
657  }
658 
659  public function saveWorkingData(
660  int $active_id,
661  ?int $pass = null,
662  bool $authorized = true
663  ): bool {
664  if ($this->questionpool_request->raw('test_answer_changed') === null) {
665  return true;
666  }
667 
668  if (is_null($pass)) {
669  $pass = ilObjTest::_getPass($active_id);
670  }
671 
672  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
673  function () use ($active_id, $pass, $authorized) {
674  $this->removeCurrentSolution($active_id, $pass, $authorized);
675 
676  foreach ($this->getSolutionListFromPostSubmit() as $orderingElement) {
677  $value1 = $orderingElement->getStorageValue1($this->getOrderingType());
678  $value2 = $orderingElement->getStorageValue2($this->getOrderingType());
679 
680  $this->saveCurrentSolution($active_id, $pass, $value1, trim((string) $value2), $authorized);
681  }
682  }
683  );
684 
685  return true;
686  }
687 
688  protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
689  {
690  if ($this->validateSolutionSubmit()) {
691  $previewSession->setParticipantsSolution(serialize($this->getSolutionListFromPostSubmit()));
692  }
693  }
694 
696  {
697  // save additional data
698  $this->db->manipulateF(
699  "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
700  ["integer"],
701  [$this->getId()]
702  );
703 
704  $this->db->manipulateF(
705  "INSERT INTO " . $this->getAdditionalTableName() . " (question_fi, ordering_type, thumb_geometry, element_height)
706  VALUES (%s, %s, %s, %s)",
707  ["integer", "text", "integer", "integer"],
708  [
709  $this->getId(),
710  $this->ordering_type,
711  $this->getThumbSize(),
712  ($this->getElementHeight() > 20) ? $this->getElementHeight() : null
713  ]
714  );
715  }
716 
717 
718  protected function getQuestionRepository(): OQRepository
719  {
720  return new OQRepository($this->db);
721  }
722 
723  public function saveAnswerSpecificDataToDb()
724  {
725  }
726 
733  public function getQuestionType(): string
734  {
735  return "assOrderingQuestion";
736  }
737 
744  public function getAdditionalTableName(): string
745  {
746  return "qpl_qst_ordering";
747  }
748 
755  public function getAnswerTableName(): string
756  {
757  return "qpl_a_ordering";
758  }
759 
764  public function getRTETextWithMediaObjects(): string
765  {
766  $text = parent::getRTETextWithMediaObjects();
767 
768  foreach ($this->getOrderingElementList() as $orderingElement) {
769  $text .= $orderingElement->getContent();
770  }
771 
772  return $text;
773  }
774 
779  public function getOrderElements(): array
780  {
781  return $this->getOrderingElementList()->getRandomIdentifierIndexedElements();
782  }
783 
784  public function getElementHeight(): ?int
785  {
786  return $this->element_height;
787  }
788 
789  public function setElementHeight(?int $a_height): void
790  {
791  $this->element_height = ($a_height < 20) ? null : $a_height;
792  }
793 
794  /*
795  * Rebuild the thumbnail images with a new thumbnail size
796  */
797  public function rebuildThumbnails(): void
798  {
799  if ($this->isImageOrderingType()) {
800  foreach ($this->getOrderElements() as $orderingElement) {
801  if ($orderingElement->getContent() !== '') {
802  $this->generateThumbForFile($this->getImagePath(), $orderingElement->getContent());
803  }
804  }
805  }
806  }
807 
808  public function getThumbPrefix(): string
809  {
810  return "thumb.";
811  }
812 
813  protected function generateThumbForFile($path, $file): void
814  {
815  $filename = $path . $file;
816  if (@file_exists($filename)) {
817  $thumbpath = $path . $this->getThumbPrefix() . $file;
818  $path_info = @pathinfo($filename);
819  $ext = "";
820  switch (strtoupper($path_info['extension'])) {
821  case 'PNG':
822  $ext = 'PNG';
823  break;
824  case 'GIF':
825  $ext = 'GIF';
826  break;
827  default:
828  $ext = 'JPEG';
829  break;
830  }
831  ilShellUtil::convertImage($filename, $thumbpath, $ext, (string) $this->getThumbSize());
832  }
833  }
834 
838  public function toJSON(): string
839  {
840  $result = [];
841  $result['id'] = $this->getId();
842  $result['type'] = (string) $this->getQuestionType();
843  $result['title'] = $this->getTitleForHTMLOutput();
844  $result['question'] = $this->formatSAQuestion($this->getQuestion());
845  $result['nr_of_tries'] = $this->getNrOfTries();
846  $result['shuffle'] = true;
847  $result['points'] = $this->getPoints();
848  $result['feedback'] = [
849  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
850  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
851  ];
852  if ($this->getOrderingType() == self::OQ_PICTURES) {
853  $result['path'] = $this->getImagePathWeb();
854  }
855 
856  $counter = 1;
857  $answers = [];
858  foreach ($this->getOrderingElementList() as $orderingElement) {
859  $answers[$counter] = $orderingElement->getContent();
860  $counter++;
861  }
862  $answers = $this->getShuffler()->transform($answers);
863  $arr = [];
864  foreach ($answers as $order => $answer) {
865  array_push($arr, [
866  "answertext" => (string) $answer,
867  "order" => (int) $order
868  ]);
869  }
870  $result['answers'] = $arr;
871 
872  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
873  $result['mobs'] = $mobs;
874 
875  return json_encode($result);
876  }
877 
883  {
884  if ($this->isImageOrderingType()) {
885  return $this->buildOrderingImagesInputGui();
886  }
887  return $this->buildOrderingTextsInputGui();
888  }
889 
890 
895  {
896  switch (true) {
897  case $formField instanceof ilAssNestedOrderingElementsInputGUI:
898  $formField->setInteractionEnabled(true);
899  $formField->setNestingEnabled($this->isOrderingTypeNested());
900  break;
901 
902  case $formField instanceof ilAssOrderingTextsInputGUI:
903  case $formField instanceof ilAssOrderingImagesInputGUI:
904  default:
905 
906  $formField->setEditElementOccuranceEnabled(true);
907  $formField->setEditElementOrderEnabled(true);
908  }
909  }
910 
914  public function initOrderingElementFormFieldLabels(ilFormPropertyGUI $formField): void
915  {
916  $formField->setInfo($this->lng->txt('ordering_answer_sequence_info'));
917  $formField->setTitle($this->lng->txt('answers'));
918  }
919 
924  {
925  $formDataConverter = $this->buildOrderingTextsFormDataConverter();
926 
927  $orderingElementInput = new ilAssOrderingTextsInputGUI(
928  $formDataConverter,
929  self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
930  );
931 
932  $this->initOrderingElementFormFieldLabels($orderingElementInput);
933 
934  return $orderingElementInput;
935  }
936 
941  {
942  $formDataConverter = $this->buildOrderingImagesFormDataConverter();
943 
944  $orderingElementInput = new ilAssOrderingImagesInputGUI(
945  $formDataConverter,
946  self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
947  );
948 
949  $orderingElementInput->setImageUploadCommand(self::ORDERING_ELEMENT_FORM_CMD_UPLOAD_IMG);
950  $orderingElementInput->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
951 
952  $this->initOrderingElementFormFieldLabels($orderingElementInput);
953 
954  return $orderingElementInput;
955  }
956 
961  {
962  $form_data_converter = $this->buildNestedOrderingFormDataConverter();
963 
964  $ordering_element_input = new ilAssNestedOrderingElementsInputGUI(
965  $form_data_converter,
966  self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
967  );
968 
969  $ordering_element_input->setUniquePrefix($this->getId());
970  $ordering_element_input->setOrderingType($this->getOrderingType());
971  $ordering_element_input->setElementImagePath($this->getImagePathWeb());
972  $ordering_element_input->setThumbPrefix($this->getThumbPrefix());
973 
974  $this->initOrderingElementFormFieldLabels($ordering_element_input);
975 
976  return $ordering_element_input;
977  }
978 
979  public function fetchSolutionListFromFormSubmissionData(array $user_solution_post): ilAssOrderingElementList
980  {
981  $ordering_gui = $this->buildNestedOrderingElementInputGui();
983  $ordering_gui->setValueByArray($user_solution_post);
984 
985  if (!$ordering_gui->checkInput()) {
986  throw new ilTestException('error on validating user solution post');
987  }
988 
989  $solution_ordering_element_list = ilAssOrderingElementList::buildInstance($this->getId());
990 
991  $stored_element_list = $this->getOrderingElementList();
992 
993  foreach ($ordering_gui->getElementList($this->getId()) as $submitted_element) {
994  $solution_element = $stored_element_list->getElementByRandomIdentifier(
995  $submitted_element->getRandomIdentifier()
996  )->getClone();
997 
998  $solution_element->setPosition($submitted_element->getPosition());
999 
1000  if ($this->isOrderingTypeNested()) {
1001  $solution_element->setIndentation($submitted_element->getIndentation());
1002  }
1003 
1004  $solution_ordering_element_list->addElement($solution_element);
1005  }
1006 
1007  return $solution_ordering_element_list;
1008  }
1009 
1011 
1013  {
1014  if ($this->postSolutionOrderingElementList === null) {
1016  $this->http->request()->getParsedBody()
1017  );
1018  $this->postSolutionOrderingElementList = $list;
1019  }
1020 
1022  }
1023 
1024  protected function calculateReachedPointsForSolution(ilAssOrderingElementList $solution_ordering_element_list): float
1025  {
1026  $reached_points = $this->getPoints();
1027 
1028  foreach ($this->getOrderingElementList() as $correct_element) {
1029  $user_element = $solution_ordering_element_list->getElementByPosition($correct_element->getPosition());
1030  if (!$correct_element->isSameElement($user_element)) {
1031  $reached_points = 0;
1032  break;
1033  }
1034  }
1035 
1036  return $reached_points;
1037  }
1038 
1039  public function getOperators(string $expression): array
1040  {
1041  return ilOperatorsExpressionMapping::getOperatorsByExpression($expression);
1042  }
1043 
1044  public function getExpressionTypes(): array
1045  {
1046  return [
1051  ];
1052  }
1053 
1054  public function getUserQuestionResult(
1055  int $active_id,
1056  int $pass
1058  $result = new ilUserQuestionResult($this, $active_id, $pass);
1059 
1060  $maxStep = $this->lookupMaxStep($active_id, $pass);
1061  if ($maxStep > 0) {
1062  $data = $this->db->queryF(
1063  "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s ORDER BY value1 ASC ",
1064  ["integer", "integer", "integer","integer"],
1065  [$active_id, $pass, $this->getId(), $maxStep]
1066  );
1067  } else {
1068  $data = $this->db->queryF(
1069  "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s ORDER BY value1 ASC ",
1070  ["integer", "integer", "integer"],
1071  [$active_id, $pass, $this->getId()]
1072  );
1073  }
1074 
1075  $elements = [];
1076  while ($row = $this->db->fetchAssoc($data)) {
1077  $newKey = explode(":", $row["value2"]);
1078 
1079  foreach ($this->getOrderingElementList() as $answer) {
1080  // Images not supported
1081  if (!$this->isOrderingTypeNested()) {
1082  if ($answer->getSolutionIdentifier() == $row["value1"]) {
1083  $elements[$row["value2"]] = $answer->getSolutionIdentifier() + 1;
1084  break;
1085  }
1086  } else {
1087  if ($answer->getRandomIdentifier() == $newKey[0]) {
1088  $elements[$row["value1"]] = $answer->getSolutionIdentifier() + 1;
1089  break;
1090  }
1091  }
1092  }
1093  }
1094 
1095  ksort($elements);
1096 
1097  foreach (array_values($elements) as $element) {
1098  $result->addKeyValue($element, $element);
1099  }
1100 
1101  $points = $this->calculateReachedPoints($active_id, $pass);
1102  $max_points = $this->getMaximumPoints();
1103 
1104  $result->setReachedPercentage(($points / $max_points) * 100);
1105 
1106  return $result;
1107  }
1108 
1116  public function getAvailableAnswerOptions($index = null)
1117  {
1118  if ($index !== null) {
1119  return $this->getOrderingElementList()->getElementByPosition($index);
1120  }
1121 
1122  return $this->getOrderingElementList()->getElements();
1123  }
1124 
1125  // fau: testNav - new function getTestQuestionConfig()
1130  // hey: refactored identifiers
1132  // hey.
1133  {
1134  // hey: refactored identifiers
1135  return parent::buildTestPresentationConfig()
1136  // hey.
1138  ->setUseUnchangedAnswerLabel($this->lng->txt('tst_unchanged_order_is_correct'));
1139  }
1140  // fau.
1141 
1142  protected function ensureImagePathExists(): void
1143  {
1144  if (!file_exists($this->getImagePath())) {
1146  }
1147  }
1148 
1152  public function fetchSolutionSubmit(array $form_submission_data_structure): array
1153  {
1154  $solution_submit = [];
1155 
1156  if (isset($form_submission_data_structure['orderresult'])) {
1157  $orderresult = $form_submission_data_structure['orderresult'];
1158 
1159  if (strlen($orderresult)) {
1160  $orderarray = explode(":", $orderresult);
1161  $ordervalue = 1;
1162  foreach ($orderarray as $index) {
1163  $idmatch = null;
1164  if (preg_match("/id_(\\d+)/", $index, $idmatch)) {
1165  $randomid = $idmatch[1];
1166  foreach ($this->getOrderingElementList() as $answeridx => $answer) {
1167  if ($answer->getRandomIdentifier() == $randomid) {
1168  $solution_submit[$answeridx] = $ordervalue;
1169  $ordervalue++;
1170  }
1171  }
1172  }
1173  }
1174  }
1175  } elseif ($this->getOrderingType() == self::OQ_NESTED_TERMS || $this->getOrderingType() == self::OQ_NESTED_PICTURES) {
1176  $index = 0;
1177  foreach ($form_submission_data_structure['content'] as $randomId => $content) {
1178  $indentation = $form_submission_data_structure['indentation'];
1179 
1180  $value1 = $index++;
1181  $value2 = implode(':', [$randomId, $indentation]);
1182 
1183  $solution_submit[$value1] = $value2;
1184  }
1185  } else {
1186  foreach ($form_submission_data_structure as $key => $value) {
1187  $matches = null;
1188  if (preg_match("/^order_(\d+)/", $key, $matches)) {
1189  if (!(preg_match("/initial_value_\d+/", $value))) {
1190  if (strlen($value)) {
1191  foreach ($this->getOrderingElementList() as $answeridx => $answer) {
1192  if ($answer->getRandomIdentifier() == $matches[1]) {
1193  $solution_submit[$answeridx] = $value;
1194  }
1195  }
1196  }
1197  }
1198  }
1199  }
1200  }
1201 
1202  return $solution_submit;
1203  }
1204 
1209  {
1210  $converter = new ilAssOrderingFormValuesObjectsConverter();
1211  $converter->setPostVar(self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR);
1212 
1213  return $converter;
1214  }
1215 
1220  {
1221  $formDataConverter = $this->buildOrderingElementFormDataConverter();
1223 
1224  $formDataConverter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
1225  $formDataConverter->setImageUrlPath($this->getImagePathWeb());
1226  $formDataConverter->setImageFsPath($this->getImagePath());
1227 
1228  if ($this->getThumbPrefix()) {
1229  $formDataConverter->setThumbnailPrefix($this->getThumbPrefix());
1230  }
1231  return $formDataConverter;
1232  }
1233 
1238  {
1239  $form_data_converter = $this->buildOrderingElementFormDataConverter();
1241  return $form_data_converter;
1242  }
1243 
1248  {
1249  $form_data_converter = $this->buildOrderingElementFormDataConverter();
1251 
1252  if ($this->getOrderingType() === self::OQ_NESTED_PICTURES) {
1253  $form_data_converter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
1254  $form_data_converter->setImageUrlPath($this->getImagePathWeb());
1255  $form_data_converter->setThumbnailPrefix($this->getThumbPrefix());
1256  }
1257 
1258  return $form_data_converter;
1259  }
1260 
1261  public function toLog(AdditionalInformationGenerator $additional_info): array
1262  {
1263  return [
1264  AdditionalInformationGenerator::KEY_QUESTION_TYPE => (string) $this->getQuestionType(),
1265  AdditionalInformationGenerator::KEY_QUESTION_TITLE => $this->getTitleForHTMLOutput(),
1266  AdditionalInformationGenerator::KEY_QUESTION_TEXT => $this->formatSAQuestion($this->getQuestion()),
1267  AdditionalInformationGenerator::KEY_QUESTION_ORDERING_NESTING_TYPE => array_reduce(
1268  $this->getOrderingTypeLangVars($this->getOrderingType()),
1269  static fn(string $string, string $lang_var) => $string . $additional_info->getTagForLangVar($lang_var),
1270  ''
1271  ),
1272  AdditionalInformationGenerator::KEY_QUESTION_REACHABLE_POINTS => $this->getPoints(),
1273  AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION => $this->getSolutionOutputForLog(),
1274  AdditionalInformationGenerator::KEY_FEEDBACK => [
1275  AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_INCOMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1276  AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_COMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1277  ]
1278  ];
1279  }
1280 
1281  private function getOrderingTypeLangVars(int $ordering_type): array
1282  {
1283  switch ($ordering_type) {
1284  case self::OQ_PICTURES:
1285  return ['qst_nested_nested_answers_off', 'oq_btn_use_order_pictures'];
1286  case self::OQ_TERMS:
1287  return ['qst_nested_nested_answers_off', 'oq_btn_use_order_terms'];
1288  case self::OQ_NESTED_PICTURES:
1289  return ['qst_nested_nested_answers_on', 'oq_btn_use_order_pictures'];
1290  case self::OQ_NESTED_TERMS:
1291  return ['qst_nested_nested_answers_on', 'oq_btn_use_order_terms'];
1292  default:
1293  return ['', ''];
1294  }
1295  }
1296 
1297  private function getSolutionOutputForLog(): string
1298  {
1299  $solution_ordering_list = $this->getOrderingElementList();
1300 
1301  $answers_gui = $this->buildNestedOrderingElementInputGui();
1303  $answers_gui->setInteractionEnabled(false);
1304  $answers_gui->setElementList($solution_ordering_list);
1305 
1306  return $answers_gui->getHTML();
1307  }
1308 
1309  protected function solutionValuesToLog(
1310  AdditionalInformationGenerator $additional_info,
1311  array $solution_values
1312  ): array {
1313  if ($solution_values === []) {
1314  return [];
1315  }
1318  $this->fetchIndexedValuesFromValuePairs($solution_values)
1319  )->getElements()
1320  );
1321  }
1322 
1323  public function solutionValuesToText(array $solution_values): array
1324  {
1325  if ($solution_values === []) {
1326  return [];
1327  }
1330  $this->fetchIndexedValuesFromValuePairs($solution_values)
1331  )->getElements()
1332  );
1333  }
1334 
1335  public function getCorrectSolutionForTextOutput(int $active_id, int $pass): array
1336  {
1338  $this->getOrderingElementList()->getElements()
1339  );
1340  }
1341 
1347  private function getElementArrayWithIdentationsForTextOutput(array $elements): array
1348  {
1349  usort(
1350  $elements,
1352  => $a->getPosition() - $b->getPosition()
1353  );
1354 
1355  return array_map(
1356  function (ilAssOrderingElement $v): string {
1357  $indentation = '';
1358  for ($i = 0;$i < $v->getIndentation();$i++) {
1359  $indentation .= ' |';
1360  }
1361  return $indentation . $v->getContent();
1362  },
1363  $elements
1364  );
1365  }
1366 }
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...
ilAssOrderingElementList $element_list_for_deferred_saving
setNrOfTries(int $a_nr_of_tries)
initOrderingElementAuthoringProperties(ilFormPropertyGUI $formField)
getSolutionValues(int $active_id, ?int $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
updateImageFile(string $existing_image_name)
solutionValuesToText(array $solution_values)
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.
setOrderingType(int $ordering_type=self::OQ_TERMS)
setIsUnchangedAnswerPossible($isUnchangedAnswerPossible)
Set if the saving of an unchanged answer is supported with an additional checkbox.
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
getCorrectSolutionForTextOutput(int $active_id, int $pass)
saveWorkingData(int $active_id, ?int $pass=null, bool $authorized=true)
removeAllImageFiles(string $image_target_path)
setOwner(int $owner=-1)
ensureNonNegativePoints(float $points)
initOrderingElementFormFieldLabels(ilFormPropertyGUI $formField)
getQuestionType()
Returns the question type of the question.
getOrderElements()
Returns the answers array.
getImagePathWeb()
Returns the web image path for web accessable images of a question.
setThumbSize(int $a_size)
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=[])
setOrderingElementList(ilAssOrderingElementList $list)
savePreviewData(ilAssQuestionPreviewSession $previewSession)
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
setComment(string $comment="")
isImageReplaced(ilAssOrderingElement $newElement, ilAssOrderingElement $oldElement)
Base Exception for all Exceptions relating to Modules/Test.
$path
Definition: ltiservices.php:30
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
getAnswer(int $index=0)
Returns the ordering element from the given position.
__construct(string $title="", string $comment="", string $author="", int $owner=-1, string $question="", protected int $ordering_type=self::OQ_TERMS)
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
getSolutionValuePairBrandedOrderingElementByRandomIdentifier(int $value1, string $value2)
static http()
Fetches the global http state from ILIAS.
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
solutionValuesToLog(AdditionalInformationGenerator $additional_info, array $solution_values)
getExpressionTypes()
Get all available expression types for a specific question.
buildHashedImageFilename(string $plain_image_filename, bool $unique=false)
fetchSolutionSubmit(array $form_submission_data_structure)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getAnswerTableName()
Returns the name of the answer table in the database.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
deleteAnswer(int $random_identifier)
$text
Definition: xapiexit.php:21
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
toJSON()
Returns a JSON representation of the question.
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $preview_session, $reached_points)
cloneQuestionTypeSpecificProperties(\assQuestion $target)
getElementArrayWithIdentationsForTextOutput(array $elements)
static getDir(string $a_dir, bool $a_rec=false, ?string $a_sub_dir="")
get directory
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="")
saveToDb(?int $original_id=null)
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
fetchIndexedValuesFromValuePairs(array $value_pairs)
$filename
Definition: buildRTE.php:78
fetchSolutionListFromFormSubmissionData(array $user_solution_post)
getSolutionMaxPass(int $active_id)
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
storeImageFile(string $upload_file, string $upload_name)
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)
setOriginalId(?int $original_id)
ilAssOrderingElementList $postSolutionOrderingElementList
getTestOutputSolutions(int $activeId, int $pass)
cloneImages(int $source_question_id, int $source_parent_id, int $target_question_id, int $target_parent_id)
setTitle(string $title="")
Class for ordering questions.
getOperators(string $expression)
Get all available operations for a specific question.
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
setLifecycle(ilAssQuestionLifecycle $lifecycle)
toLog(AdditionalInformationGenerator $additional_info)
buildTestPresentationConfig()
Get the test question configuration.
static rename(string $a_source, string $a_target)
getSolutionOrderingElementList(array $indexed_solution_values)
calculateReachedPoints(int $active_id, ?int $pass=null, bool $authorized_solution=true)
saveToDb(?int $original_id=null)
getSolutionOrderingElementListForTestOutput(ilAssNestedOrderingElementsInputGUI $input_gui, array $last_post, int $active_id, int $pass)
lookupMaxStep(int $active_id, int $pass)
setAuthor(string $author="")
getOrderingElementListForSolutionOutput(bool $force_correct_solution, int $active_id, ?int $pass_index)
getUserQuestionResult(int $active_id, int $pass)
Get the user solution for a question by active_id and the test pass.
calculateReachedPointsForSolution(ilAssOrderingElementList $solution_ordering_element_list)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
loadFromDb($question_id)
Loads a assOrderingQuestion object from a database.
getSolutionValuePairBrandedOrderingElementBySolutionIdentifier(int $value1, string $value2)
ensureValidIdentifiers(ilAssOrderingElement $element)
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
getOrderingTypeLangVars(int $ordering_type)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getAdditionalTableName()
Returns the name of the additional question data table in the database.
setQuestion(string $question="")
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $preview_session)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...