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