ILIAS  trunk Revision v11.0_alpha-1753-gb21ca8c4367
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
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 = [];
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  return $target;
172  }
173 
174  public function cloneImages(
175  int $source_question_id,
176  int $source_parent_id,
177  int $target_question_id,
178  int $target_parent_id
179  ): void {
180  if (!$this->isImageOrderingType()) {
181  return;
182  }
183 
184  $image_source_path = $this->getImagePath($source_question_id, $source_parent_id);
185  $image_target_path = $this->getImagePath($target_question_id, $target_parent_id);
186 
187  if (!file_exists($image_target_path)) {
188  ilFileUtils::makeDirParents($image_target_path);
189  } else {
190  $this->removeAllImageFiles($image_target_path);
191  }
192  foreach ($this->getOrderingElementList() as $element) {
193  $filename = $element->getContent();
194 
195  if ($filename === '') {
196  continue;
197  }
198 
199  if (!file_exists($image_source_path . $filename)
200  || !copy($image_source_path . $filename, $image_target_path . $filename)) {
201  $this->log->root()->warning('Image could not be cloned for object for question: ' . $target_question_id);
202  }
203  if (!file_exists($image_source_path . $this->getThumbPrefix() . $filename)
204  || !copy($image_source_path . $this->getThumbPrefix() . $filename, $image_target_path . $this->getThumbPrefix() . $filename)) {
205  $this->log->root()->warning('Image thumbnails could not be cloned for object for question: ' . $target_question_id);
206  }
207  }
208  }
209 
210  protected function getValidOrderingTypes(): array
211  {
212  return [
213  self::OQ_PICTURES,
214  self::OQ_TERMS,
215  self::OQ_NESTED_PICTURES,
216  self::OQ_NESTED_TERMS
217  ];
218  }
219 
220  public function setOrderingType(int $ordering_type = self::OQ_TERMS)
221  {
222  if (!in_array($ordering_type, $this->getValidOrderingTypes())) {
223  throw new \InvalidArgumentException('Must be valid ordering type.');
224  }
225  $this->ordering_type = $ordering_type;
226  }
227 
228  public function getOrderingType(): int
229  {
230  return $this->ordering_type;
231  }
232 
233  public function isOrderingTypeNested(): bool
234  {
235  $nested = [
236  self::OQ_NESTED_TERMS,
237  self::OQ_NESTED_PICTURES
238  ];
239  return in_array($this->getOrderingType(), $nested);
240  }
241 
242  public function isImageOrderingType(): bool
243  {
244  $with_images = [
245  self::OQ_PICTURES,
246  self::OQ_NESTED_PICTURES
247  ];
248  return in_array($this->getOrderingType(), $with_images);
249  }
250 
251  public function setContentType($ct)
252  {
253  if (!in_array($ct, [
254  self::OQ_CT_PICTURES,
255  self::OQ_CT_TERMS
256  ])) {
257  throw new \InvalidArgumentException("use OQ content-type", 1);
258  }
259  if ($ct == self::OQ_CT_PICTURES) {
260  if ($this->isOrderingTypeNested()) {
261  $this->setOrderingType(self::OQ_NESTED_PICTURES);
262  } else {
263  $this->setOrderingType(self::OQ_PICTURES);
264  }
265  $this->setThumbSize($this->getThumbSize());
266  }
267  if ($ct == self::OQ_CT_TERMS) {
268  if ($this->isOrderingTypeNested()) {
269  $this->setOrderingType(self::OQ_NESTED_TERMS);
270  } else {
271  $this->setOrderingType(self::OQ_TERMS);
272  }
273  }
274  }
275 
276  public function setNestingType(bool $nesting)
277  {
278  if ($nesting) {
279  if ($this->isImageOrderingType()) {
280  $this->setOrderingType(self::OQ_NESTED_PICTURES);
281  } else {
282  $this->setOrderingType(self::OQ_NESTED_TERMS);
283  }
284  } else {
285  if ($this->isImageOrderingType()) {
286  $this->setOrderingType(self::OQ_PICTURES);
287  } else {
288  $this->setOrderingType(self::OQ_TERMS);
289  }
290  }
291  }
292 
293  public function hasOrderingTypeUploadSupport(): bool
294  {
295  return $this->isImageOrderingType();
296  }
297 
299  bool $force_correct_solution,
300  int $active_id,
301  ?int $pass_index
303  if ($force_correct_solution || !$active_id || $pass_index === null) {
304  return $this->getOrderingElementList();
305  }
306 
307  $solution_values = $this->getSolutionValues($active_id, $pass_index);
308 
309  if (!count($solution_values)) {
310  return $this->getShuffledOrderingElementList();
311  }
312 
313  return $this->getSolutionOrderingElementList($this->fetchIndexedValuesFromValuePairs($solution_values));
314  }
315 
318  array $last_post,
319  int $active_id,
320  int $pass
322  if ($input_gui->isPostSubmit($last_post)) {
323  return $this->fetchSolutionListFromFormSubmissionData($last_post);
324  }
325  $indexedSolutionValues = $this->fetchIndexedValuesFromValuePairs(
326  // hey: prevPassSolutions - obsolete due to central check
327  $this->getTestOutputSolutions($active_id, $pass)
328  // hey.
329  );
330 
331  if (count($indexedSolutionValues)) {
332  return $this->getSolutionOrderingElementList($indexedSolutionValues);
333  }
334 
335  return $this->getShuffledOrderingElementList();
336  }
337 
339  int $value1,
340  string $value2
342  $value = explode(':', $value2);
343 
344  $random_identifier = (int) $value[0];
345  $selected_position = $value1;
346  $selected_indentation = (int) $value[1];
347 
348  $element = $this->getOrderingElementList()->getElementByRandomIdentifier($random_identifier)->getClone();
349 
350  $element->setPosition($selected_position);
351  $element->setIndentation($selected_indentation);
352 
353  return $element;
354  }
355 
357  int $value1,
358  string $value2
360  $solution_identifier = $value1;
361  $selected_position = ($value2 - 1);
362  $selected_indentation = 0;
363 
364  $element = $this->getOrderingElementList()->getElementBySolutionIdentifier($solution_identifier)->getClone();
365 
366  $element->setPosition($selected_position);
367  $element->setIndentation($selected_indentation);
368 
369  return $element;
370  }
371 
375  public function getSolutionOrderingElementList(array $indexed_solution_values): ilAssOrderingElementList
376  {
377  $solution_ordering_list = new ilAssOrderingElementList();
378  $solution_ordering_list->setQuestionId($this->getId());
379 
380  foreach ($indexed_solution_values as $value1 => $value2) {
381  if ($this->isOrderingTypeNested()) {
382  $element = $this->getSolutionValuePairBrandedOrderingElementByRandomIdentifier($value1, $value2);
383  } else {
384  $element = $this->getSolutionValuePairBrandedOrderingElementBySolutionIdentifier($value1, $value2);
385  }
386 
387  $solution_ordering_list->addElement($element);
388  }
389 
390  if (!$this->getOrderingElementList()->hasSameElementSetByRandomIdentifiers($solution_ordering_list)) {
391  throw new ilTestQuestionPoolException('inconsistent solution values given');
392  }
393 
394  return $solution_ordering_list;
395  }
396 
403  {
404  $shuffledRandomIdentifierIndex = $this->getShuffler()->transform(
405  $this->getOrderingElementList()->getRandomIdentifierIndex()
406  );
407 
408  $shuffledElementList = $this->getOrderingElementList()->getClone();
409  $shuffledElementList->reorderByRandomIdentifiers($shuffledRandomIdentifierIndex);
410  $shuffledElementList->resetElementsIndentations();
411 
412  return $shuffledElementList;
413  }
414 
419  {
420  return $this->getRepository()->getOrderingList($this->getId());
421  }
422 
427  {
428  if ($this->getId() <= 0) {
429  $this->element_list_for_deferred_saving = $list;
430  return;
431  }
432  $list = $list->withQuestionId($this->getId());
433  $elements = $list->getElements();
434  $nu = [];
435  foreach ($elements as $e) {
436  $nu[] = $list->ensureValidIdentifiers($e);
437  }
438  $this->getRepository()->updateOrderingList(
439  $list->withElements($nu)
440  );
441  }
442 
449  public function getAnswer(int $index = 0): ?ilAssOrderingElement
450  {
451  if (!$this->getOrderingElementList()->elementExistByPosition($index)) {
452  return null;
453  }
454 
455  return $this->getOrderingElementList()->getElementByPosition($index);
456  }
457 
458  public function deleteAnswer(int $random_identifier): void
459  {
460  $this->getOrderingElementList()->removeElement(
461  $this->getOrderingElementList()->getElementByRandomIdentifier($random_identifier)
462  );
463  $this->getOrderingElementList()->saveToDb();
464  }
465 
466  public function getAnswerCount(): int
467  {
468  return $this->getOrderingElementList()->countElements();
469  }
470 
471  public function calculateReachedPoints(
472  int $active_id,
473  ?int $pass = null,
474  bool $authorized_solution = true
475  ): float {
476  if ($pass === null) {
477  $pass = $this->getSolutionMaxPass($active_id);
478  }
479 
480  $solution_value_pairs = $this->getSolutionValues($active_id, $pass, $authorized_solution);
481 
482  if ($solution_value_pairs === []) {
483  return 0.0;
484  }
485 
486  $solution_ordering_element_list = $this->getSolutionOrderingElementList(
487  $this->fetchIndexedValuesFromValuePairs($solution_value_pairs)
488  );
489 
490  return $this->calculateReachedPointsForSolution($solution_ordering_element_list);
491  }
492 
494  {
495  if (!$preview_session->hasParticipantSolution()) {
496  return 0.0;
497  }
498 
499  $solution_ordering_element_list = unserialize(
500  $preview_session->getParticipantsSolution(),
501  ['allowed_classes' => true]
502  );
503 
504  $reached_points = $this->deductHintPointsFromReachedPoints(
505  $preview_session,
506  $this->calculateReachedPointsForSolution($solution_ordering_element_list)
507  );
508 
509  return $this->ensureNonNegativePoints($reached_points);
510  }
511 
512  public function getMaximumPoints(): float
513  {
514  return $this->getPoints();
515  }
516 
517  /*
518  * Returns the encrypted save filename of a matching picture
519  * Images are saved with an encrypted filename to prevent users from
520  * cheating by guessing the solution from the image filename
521  *
522  * @param string $filename Original filename
523  * @return string Encrypted filename
524  */
525  public function getEncryptedFilename($filename): string
526  {
527  $extension = "";
528  if (preg_match("/.*\\.(\\w+)$/", $filename, $matches)) {
529  $extension = $matches[1];
530  }
531  return md5($filename) . "." . $extension;
532  }
533 
534  protected function cleanImagefiles(): void
535  {
536  if ($this->getOrderingType() == self::OQ_PICTURES) {
537  if (@file_exists($this->getImagePath())) {
538  $contents = ilFileUtils::getDir($this->getImagePath());
539  foreach ($contents as $f) {
540  if (strcmp($f['type'], 'file') == 0) {
541  $found = false;
542  foreach ($this->getOrderingElementList() as $orderElement) {
543  if (strcmp($f['entry'], $orderElement->getContent()) == 0) {
544  $found = true;
545  }
546  if (strcmp($f['entry'], $this->getThumbPrefix() . $orderElement->getContent()) == 0) {
547  $found = true;
548  }
549  }
550  if (!$found) {
551  if (@file_exists($this->getImagePath() . $f['entry'])) {
552  @unlink($this->getImagePath() . $f['entry']);
553  }
554  }
555  }
556  }
557  }
558  } else {
559  if (@file_exists($this->getImagePath())) {
561  }
562  }
563  }
564 
565  /*
566  * Deletes an imagefile from the system if the file is deleted manually
567  *
568  * @param string $filename Image file filename
569  * @return boolean Success
570  */
571  public function dropImageFile($imageFilename)
572  {
573  if (!strlen($imageFilename)) {
574  return false;
575  }
576 
577  $result = @unlink($this->getImagePath() . $imageFilename);
578  $result = $result && @unlink($this->getImagePath() . $this->getThumbPrefix() . $imageFilename);
579 
580  return $result;
581  }
582 
583  public function isImageFileStored($imageFilename): bool
584  {
585  if (!strlen($imageFilename)) {
586  return false;
587  }
588 
589  if (!file_exists($this->getImagePath() . $imageFilename)) {
590  return false;
591  }
592 
593  return is_file($this->getImagePath() . $imageFilename);
594  }
595 
596  public function isImageReplaced(ilAssOrderingElement $newElement, ilAssOrderingElement $oldElement): bool
597  {
598  if (!$this->hasOrderingTypeUploadSupport()) {
599  return false;
600  }
601 
602  if (!$newElement->getContent()) {
603  return false;
604  }
605 
606  return $newElement->getContent() != $oldElement->getContent();
607  }
608 
609 
610  public function storeImageFile(string $upload_file, string $upload_name): ?string
611  {
612  $name_parts = explode(".", $upload_name);
613  $suffix = strtolower(array_pop($name_parts));
614  if (!in_array($suffix, self::VALID_UPLOAD_SUFFIXES)) {
615  return null;
616  }
617 
618  $this->ensureImagePathExists();
619  $target_filename = $this->buildHashedImageFilename($upload_name, true);
620  $target_filepath = $this->getImagePath() . $target_filename;
621  if (ilFileUtils::moveUploadedFile($upload_file, $target_filename, $target_filepath)) {
622  $thumb_path = $this->getImagePath() . $this->getThumbPrefix() . $target_filename;
623  ilShellUtil::convertImage($target_filepath, $thumb_path, "JPEG", (string) $this->getThumbSize());
624 
625  return $target_filename;
626  }
627 
628  return null;
629  }
630 
631  public function updateImageFile(string $existing_image_name): ?string
632  {
633  $existing_image_path = $this->getImagePath() . $existing_image_name;
634  $target_filename = $this->buildHashedImageFilename($existing_image_name, true);
635  $target_filepath = $this->getImagePath() . $target_filename;
636  if (ilFileUtils::rename($existing_image_path, $target_filepath)) {
637  unlink($this->getImagePath() . $this->getThumbPrefix() . $existing_image_name);
638  $thumb_path = $this->getImagePath() . $this->getThumbPrefix() . $target_filename;
639  ilShellUtil::convertImage($target_filepath, $thumb_path, "JPEG", (string) $this->getThumbSize());
640 
641  return $target_filename;
642  }
643 
644  return $existing_image_name;
645  }
646 
647  public function validateSolutionSubmit(): bool
648  {
649  $submittedSolutionList = $this->getSolutionListFromPostSubmit();
650 
651  if (!$submittedSolutionList->hasElements()) {
652  return true;
653  }
654 
655  return $this->getOrderingElementList()->hasSameElementSetByRandomIdentifiers($submittedSolutionList);
656  }
657 
658  public function saveWorkingData(
659  int $active_id,
660  ?int $pass = null,
661  bool $authorized = true
662  ): bool {
663  if ($this->questionpool_request->raw('test_answer_changed') === null) {
664  return true;
665  }
666 
667  if (is_null($pass)) {
668  $pass = ilObjTest::_getPass($active_id);
669  }
670 
671  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
672  function () use ($active_id, $pass, $authorized) {
673  $this->removeCurrentSolution($active_id, $pass, $authorized);
674 
675  foreach ($this->getSolutionListFromPostSubmit() as $orderingElement) {
676  $value1 = $orderingElement->getStorageValue1($this->getOrderingType());
677  $value2 = $orderingElement->getStorageValue2($this->getOrderingType());
678 
679  $this->saveCurrentSolution($active_id, $pass, $value1, trim((string) $value2), $authorized);
680  }
681  }
682  );
683 
684  return true;
685  }
686 
687  protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
688  {
689  if ($this->validateSolutionSubmit()) {
690  $previewSession->setParticipantsSolution(serialize($this->getSolutionListFromPostSubmit()));
691  }
692  }
693 
695  {
696  // save additional data
697  $this->db->manipulateF(
698  "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
699  ["integer"],
700  [$this->getId()]
701  );
702 
703  $this->db->manipulateF(
704  "INSERT INTO " . $this->getAdditionalTableName() . " (question_fi, ordering_type, thumb_geometry, element_height)
705  VALUES (%s, %s, %s, %s)",
706  ["integer", "text", "integer", "integer"],
707  [
708  $this->getId(),
709  $this->ordering_type,
710  $this->getThumbSize(),
711  ($this->getElementHeight() > 20) ? $this->getElementHeight() : null
712  ]
713  );
714  }
715 
716 
717  protected function getQuestionRepository(): OQRepository
718  {
719  return new OQRepository($this->db);
720  }
721 
722  public function saveAnswerSpecificDataToDb()
723  {
724  }
725 
732  public function getQuestionType(): string
733  {
734  return "assOrderingQuestion";
735  }
736 
743  public function getAdditionalTableName(): string
744  {
745  return "qpl_qst_ordering";
746  }
747 
754  public function getAnswerTableName(): string
755  {
756  return "qpl_a_ordering";
757  }
758 
763  public function getRTETextWithMediaObjects(): string
764  {
765  $text = parent::getRTETextWithMediaObjects();
766 
767  foreach ($this->getOrderingElementList() as $orderingElement) {
768  $text .= $orderingElement->getContent();
769  }
770 
771  return $text;
772  }
773 
778  public function getOrderElements(): array
779  {
780  return $this->getOrderingElementList()->getRandomIdentifierIndexedElements();
781  }
782 
783  public function getElementHeight(): ?int
784  {
785  return $this->element_height;
786  }
787 
788  public function setElementHeight(?int $a_height): void
789  {
790  $this->element_height = ($a_height < 20) ? null : $a_height;
791  }
792 
793  /*
794  * Rebuild the thumbnail images with a new thumbnail size
795  */
796  public function rebuildThumbnails(): void
797  {
798  if ($this->isImageOrderingType()) {
799  foreach ($this->getOrderElements() as $orderingElement) {
800  if ($orderingElement->getContent() !== '') {
801  $this->generateThumbForFile($this->getImagePath(), $orderingElement->getContent());
802  }
803  }
804  }
805  }
806 
807  public function getThumbPrefix(): string
808  {
809  return "thumb.";
810  }
811 
812  protected function generateThumbForFile($path, $file): void
813  {
814  $filename = $path . $file;
815  if (@file_exists($filename)) {
816  $thumbpath = $path . $this->getThumbPrefix() . $file;
817  $path_info = @pathinfo($filename);
818  $ext = "";
819  switch (strtoupper($path_info['extension'])) {
820  case 'PNG':
821  $ext = 'PNG';
822  break;
823  case 'GIF':
824  $ext = 'GIF';
825  break;
826  default:
827  $ext = 'JPEG';
828  break;
829  }
830  ilShellUtil::convertImage($filename, $thumbpath, $ext, (string) $this->getThumbSize());
831  }
832  }
833 
837  public function toJSON(): string
838  {
839  $result = [];
840  $result['id'] = $this->getId();
841  $result['type'] = (string) $this->getQuestionType();
842  $result['title'] = $this->getTitleForHTMLOutput();
843  $result['question'] = $this->formatSAQuestion($this->getQuestion());
844  $result['nr_of_tries'] = $this->getNrOfTries();
845  $result['shuffle'] = true;
846  $result['points'] = $this->getPoints();
847  $result['feedback'] = [
848  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
849  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
850  ];
851  if ($this->getOrderingType() == self::OQ_PICTURES) {
852  $result['path'] = $this->getImagePathWeb();
853  }
854 
855  $counter = 1;
856  $answers = [];
857  foreach ($this->getOrderingElementList() as $orderingElement) {
858  $answers[$counter] = $orderingElement->getContent();
859  $counter++;
860  }
861  $answers = $this->getShuffler()->transform($answers);
862  $arr = [];
863  foreach ($answers as $order => $answer) {
864  array_push($arr, [
865  "answertext" => (string) $answer,
866  "order" => (int) $order
867  ]);
868  }
869  $result['answers'] = $arr;
870 
871  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
872  $result['mobs'] = $mobs;
873 
874  return json_encode($result);
875  }
876 
882  {
883  if ($this->isImageOrderingType()) {
884  return $this->buildOrderingImagesInputGui();
885  }
886  return $this->buildOrderingTextsInputGui();
887  }
888 
889 
894  {
895  switch (true) {
896  case $formField instanceof ilAssNestedOrderingElementsInputGUI:
897  $formField->setInteractionEnabled(true);
898  $formField->setNestingEnabled($this->isOrderingTypeNested());
899  break;
900 
901  case $formField instanceof ilAssOrderingTextsInputGUI:
902  case $formField instanceof ilAssOrderingImagesInputGUI:
903  default:
904 
905  $formField->setEditElementOccuranceEnabled(true);
906  $formField->setEditElementOrderEnabled(true);
907  }
908  }
909 
913  public function initOrderingElementFormFieldLabels(ilFormPropertyGUI $formField): void
914  {
915  $formField->setInfo($this->lng->txt('ordering_answer_sequence_info'));
916  $formField->setTitle($this->lng->txt('answers'));
917  }
918 
923  {
924  $formDataConverter = $this->buildOrderingTextsFormDataConverter();
925 
926  $orderingElementInput = new ilAssOrderingTextsInputGUI(
927  $formDataConverter,
928  self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
929  );
930 
931  $this->initOrderingElementFormFieldLabels($orderingElementInput);
932 
933  return $orderingElementInput;
934  }
935 
940  {
941  $formDataConverter = $this->buildOrderingImagesFormDataConverter();
942 
943  $orderingElementInput = new ilAssOrderingImagesInputGUI(
944  $formDataConverter,
945  self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
946  );
947 
948  $orderingElementInput->setImageUploadCommand(self::ORDERING_ELEMENT_FORM_CMD_UPLOAD_IMG);
949  $orderingElementInput->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
950 
951  $this->initOrderingElementFormFieldLabels($orderingElementInput);
952 
953  return $orderingElementInput;
954  }
955 
960  {
961  $form_data_converter = $this->buildNestedOrderingFormDataConverter();
962 
963  $ordering_element_input = new ilAssNestedOrderingElementsInputGUI(
964  $form_data_converter,
965  self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
966  );
967 
968  $ordering_element_input->setUniquePrefix($this->getId());
969  $ordering_element_input->setOrderingType($this->getOrderingType());
970  $ordering_element_input->setElementImagePath($this->getImagePathWeb());
971  $ordering_element_input->setThumbPrefix($this->getThumbPrefix());
972 
973  $this->initOrderingElementFormFieldLabels($ordering_element_input);
974 
975  return $ordering_element_input;
976  }
977 
978  public function fetchSolutionListFromFormSubmissionData(array $user_solution_post): ilAssOrderingElementList
979  {
980  $ordering_gui = $this->buildNestedOrderingElementInputGui();
982  $ordering_gui->setValueByArray($user_solution_post);
983 
984  if (!$ordering_gui->checkInput()) {
985  throw new ilTestException('error on validating user solution post');
986  }
987 
988  $solution_ordering_element_list = ilAssOrderingElementList::buildInstance($this->getId());
989 
990  $stored_element_list = $this->getOrderingElementList();
991 
992  foreach ($ordering_gui->getElementList($this->getId()) as $submitted_element) {
993  $solution_element = $stored_element_list->getElementByRandomIdentifier(
994  $submitted_element->getRandomIdentifier()
995  )->getClone();
996 
997  $solution_element->setPosition($submitted_element->getPosition());
998 
999  if ($this->isOrderingTypeNested()) {
1000  $solution_element->setIndentation($submitted_element->getIndentation());
1001  }
1002 
1003  $solution_ordering_element_list->addElement($solution_element);
1004  }
1005 
1006  return $solution_ordering_element_list;
1007  }
1008 
1010 
1012  {
1013  if ($this->postSolutionOrderingElementList === null) {
1015  $this->http->request()->getParsedBody()
1016  );
1017  $this->postSolutionOrderingElementList = $list;
1018  }
1019 
1021  }
1022 
1023  protected function calculateReachedPointsForSolution(ilAssOrderingElementList $solution_ordering_element_list): float
1024  {
1025  $reached_points = $this->getPoints();
1026 
1027  foreach ($this->getOrderingElementList() as $correct_element) {
1028  $user_element = $solution_ordering_element_list->getElementByPosition($correct_element->getPosition());
1029  if (!$correct_element->isSameElement($user_element)) {
1030  $reached_points = 0;
1031  break;
1032  }
1033  }
1034 
1035  return $reached_points;
1036  }
1037 
1038  public function getOperators(string $expression): array
1039  {
1040  return ilOperatorsExpressionMapping::getOperatorsByExpression($expression);
1041  }
1042 
1043  public function getExpressionTypes(): array
1044  {
1045  return [
1050  ];
1051  }
1052 
1053  public function getUserQuestionResult(
1054  int $active_id,
1055  int $pass
1057  $result = new ilUserQuestionResult($this, $active_id, $pass);
1058 
1059  $maxStep = $this->lookupMaxStep($active_id, $pass);
1060  if ($maxStep > 0) {
1061  $data = $this->db->queryF(
1062  "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s ORDER BY value1 ASC ",
1063  ["integer", "integer", "integer","integer"],
1064  [$active_id, $pass, $this->getId(), $maxStep]
1065  );
1066  } else {
1067  $data = $this->db->queryF(
1068  "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s ORDER BY value1 ASC ",
1069  ["integer", "integer", "integer"],
1070  [$active_id, $pass, $this->getId()]
1071  );
1072  }
1073 
1074  $elements = [];
1075  while ($row = $this->db->fetchAssoc($data)) {
1076  $newKey = explode(":", $row["value2"]);
1077 
1078  foreach ($this->getOrderingElementList() as $answer) {
1079  // Images not supported
1080  if (!$this->isOrderingTypeNested()) {
1081  if ($answer->getSolutionIdentifier() == $row["value1"]) {
1082  $elements[$row["value2"]] = $answer->getSolutionIdentifier() + 1;
1083  break;
1084  }
1085  } else {
1086  if ($answer->getRandomIdentifier() == $newKey[0]) {
1087  $elements[$row["value1"]] = $answer->getSolutionIdentifier() + 1;
1088  break;
1089  }
1090  }
1091  }
1092  }
1093 
1094  ksort($elements);
1095 
1096  foreach (array_values($elements) as $element) {
1097  $result->addKeyValue($element, $element);
1098  }
1099 
1100  $points = $this->calculateReachedPoints($active_id, $pass);
1101  $max_points = $this->getMaximumPoints();
1102 
1103  $result->setReachedPercentage(($points / $max_points) * 100);
1104 
1105  return $result;
1106  }
1107 
1115  public function getAvailableAnswerOptions($index = null)
1116  {
1117  if ($index !== null) {
1118  return $this->getOrderingElementList()->getElementByPosition($index);
1119  }
1120 
1121  return $this->getOrderingElementList()->getElements();
1122  }
1123 
1124  // fau: testNav - new function getTestQuestionConfig()
1129  // hey: refactored identifiers
1131  // hey.
1132  {
1133  // hey: refactored identifiers
1134  return parent::buildTestPresentationConfig()
1135  // hey.
1137  ->setUseUnchangedAnswerLabel($this->lng->txt('tst_unchanged_order_is_correct'));
1138  }
1139  // fau.
1140 
1141  protected function ensureImagePathExists(): void
1142  {
1143  if (!file_exists($this->getImagePath())) {
1145  }
1146  }
1147 
1151  public function fetchSolutionSubmit(array $form_submission_data_structure): array
1152  {
1153  $solution_submit = [];
1154 
1155  if (isset($form_submission_data_structure['orderresult'])) {
1156  $orderresult = $form_submission_data_structure['orderresult'];
1157 
1158  if (strlen($orderresult)) {
1159  $orderarray = explode(":", $orderresult);
1160  $ordervalue = 1;
1161  foreach ($orderarray as $index) {
1162  $idmatch = null;
1163  if (preg_match("/id_(\\d+)/", $index, $idmatch)) {
1164  $randomid = $idmatch[1];
1165  foreach ($this->getOrderingElementList() as $answeridx => $answer) {
1166  if ($answer->getRandomIdentifier() == $randomid) {
1167  $solution_submit[$answeridx] = $ordervalue;
1168  $ordervalue++;
1169  }
1170  }
1171  }
1172  }
1173  }
1174  } elseif ($this->getOrderingType() == self::OQ_NESTED_TERMS || $this->getOrderingType() == self::OQ_NESTED_PICTURES) {
1175  $index = 0;
1176  foreach ($form_submission_data_structure['content'] as $randomId => $content) {
1177  $indentation = $form_submission_data_structure['indentation'];
1178 
1179  $value1 = $index++;
1180  $value2 = implode(':', [$randomId, $indentation]);
1181 
1182  $solution_submit[$value1] = $value2;
1183  }
1184  } else {
1185  foreach ($form_submission_data_structure as $key => $value) {
1186  $matches = null;
1187  if (preg_match("/^order_(\d+)/", $key, $matches)) {
1188  if (!(preg_match("/initial_value_\d+/", $value))) {
1189  if (strlen($value)) {
1190  foreach ($this->getOrderingElementList() as $answeridx => $answer) {
1191  if ($answer->getRandomIdentifier() == $matches[1]) {
1192  $solution_submit[$answeridx] = $value;
1193  }
1194  }
1195  }
1196  }
1197  }
1198  }
1199  }
1200 
1201  return $solution_submit;
1202  }
1203 
1208  {
1209  $converter = new ilAssOrderingFormValuesObjectsConverter();
1210  $converter->setPostVar(self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR);
1211 
1212  return $converter;
1213  }
1214 
1219  {
1220  $formDataConverter = $this->buildOrderingElementFormDataConverter();
1222 
1223  $formDataConverter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
1224  $formDataConverter->setImageUrlPath($this->getImagePathWeb());
1225  $formDataConverter->setImageFsPath($this->getImagePath());
1226 
1227  if ($this->getThumbPrefix()) {
1228  $formDataConverter->setThumbnailPrefix($this->getThumbPrefix());
1229  }
1230  return $formDataConverter;
1231  }
1232 
1237  {
1238  $form_data_converter = $this->buildOrderingElementFormDataConverter();
1240  return $form_data_converter;
1241  }
1242 
1247  {
1248  $form_data_converter = $this->buildOrderingElementFormDataConverter();
1250 
1251  if ($this->getOrderingType() === self::OQ_NESTED_PICTURES) {
1252  $form_data_converter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
1253  $form_data_converter->setImageUrlPath($this->getImagePathWeb());
1254  $form_data_converter->setThumbnailPrefix($this->getThumbPrefix());
1255  }
1256 
1257  return $form_data_converter;
1258  }
1259 
1260  public function toLog(AdditionalInformationGenerator $additional_info): array
1261  {
1262  return [
1263  AdditionalInformationGenerator::KEY_QUESTION_TYPE => (string) $this->getQuestionType(),
1264  AdditionalInformationGenerator::KEY_QUESTION_TITLE => $this->getTitleForHTMLOutput(),
1265  AdditionalInformationGenerator::KEY_QUESTION_TEXT => $this->formatSAQuestion($this->getQuestion()),
1266  AdditionalInformationGenerator::KEY_QUESTION_ORDERING_NESTING_TYPE => array_reduce(
1267  $this->getOrderingTypeLangVars($this->getOrderingType()),
1268  static fn(string $string, string $lang_var) => $string . $additional_info->getTagForLangVar($lang_var),
1269  ''
1270  ),
1271  AdditionalInformationGenerator::KEY_QUESTION_REACHABLE_POINTS => $this->getPoints(),
1272  AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION => $this->getSolutionOutputForLog(),
1273  AdditionalInformationGenerator::KEY_FEEDBACK => [
1274  AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_INCOMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1275  AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_COMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1276  ]
1277  ];
1278  }
1279 
1280  private function getOrderingTypeLangVars(int $ordering_type): array
1281  {
1282  switch ($ordering_type) {
1283  case self::OQ_PICTURES:
1284  return ['qst_nested_nested_answers_off', 'oq_btn_use_order_pictures'];
1285  case self::OQ_TERMS:
1286  return ['qst_nested_nested_answers_off', 'oq_btn_use_order_terms'];
1287  case self::OQ_NESTED_PICTURES:
1288  return ['qst_nested_nested_answers_on', 'oq_btn_use_order_pictures'];
1289  case self::OQ_NESTED_TERMS:
1290  return ['qst_nested_nested_answers_on', 'oq_btn_use_order_terms'];
1291  default:
1292  return ['', ''];
1293  }
1294  }
1295 
1296  private function getSolutionOutputForLog(): string
1297  {
1298  $solution_ordering_list = $this->getOrderingElementList();
1299 
1300  $answers_gui = $this->buildNestedOrderingElementInputGui();
1302  $answers_gui->setInteractionEnabled(false);
1303  $answers_gui->setElementList($solution_ordering_list);
1304 
1305  return $answers_gui->getHTML();
1306  }
1307 
1308  protected function solutionValuesToLog(
1309  AdditionalInformationGenerator $additional_info,
1310  array $solution_values
1311  ): array {
1312  if ($solution_values === []) {
1313  return [];
1314  }
1317  $this->fetchIndexedValuesFromValuePairs($solution_values)
1318  )->getElements()
1319  );
1320  }
1321 
1322  public function solutionValuesToText(array $solution_values): array
1323  {
1324  if ($solution_values === []) {
1325  return [];
1326  }
1329  $this->fetchIndexedValuesFromValuePairs($solution_values)
1330  )->getElements()
1331  );
1332  }
1333 
1334  public function getCorrectSolutionForTextOutput(int $active_id, int $pass): array
1335  {
1337  $this->getOrderingElementList()->getElements()
1338  );
1339  }
1340 
1346  private function getElementArrayWithIdentationsForTextOutput(array $elements): array
1347  {
1348  usort(
1349  $elements,
1351  => $a->getPosition() - $b->getPosition()
1352  );
1353 
1354  return array_map(
1355  function (ilAssOrderingElement $v): string {
1356  $indentation = '';
1357  for ($i = 0;$i < $v->getIndentation();$i++) {
1358  $indentation .= ' |';
1359  }
1360  return $indentation . $v->getContent();
1361  },
1362  $elements
1363  );
1364  }
1365 }
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:29
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
getAnswer(int $index=0)
Returns the ordering element from the given position.
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
__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)
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="")
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...