ILIAS  trunk Revision v11.0_alpha-1723-g8e69f309bab
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
class.assMatchingQuestionGUI.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
38 {
39  public function __construct($id = -1)
40  {
42  $this->object = new assMatchingQuestion();
43  $this->setErrorMessage($this->lng->txt('msg_form_save_error'));
44  if ($id >= 0) {
45  $this->object->loadFromDb($id);
46  }
47  }
48 
52  protected function writePostData(bool $always = false): int
53  {
54  $hasErrors = (!$always) ? $this->editQuestion(true) : false;
55  if (!$hasErrors) {
59  $this->saveTaxonomyAssignments();
60  return 0;
61  }
62  return 1;
63  }
64 
65  public function writeAnswerSpecificPostData(ilPropertyFormGUI $form): void
66  {
67  // Delete all existing answers and create new answers from the form data
68  $this->object->flushMatchingPairs();
69  $this->object->flushTerms();
70  $this->object->flushDefinitions();
71 
72  $kindlyTo = $this->refinery->kindlyTo();
73 
74  $uploads = $this->request_data_collector->getProcessedUploads();
75  $allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif'];
76 
77  if ($this->request_data_collector->isset('terms')) {
78  $terms = $this->request_data_collector->raw('terms');
79  $answers = $this->forms_helper->transformArray($terms, 'answer', $kindlyTo->string());
80  $terms_image_names = $this->forms_helper->transformArray($terms, 'imagename', $kindlyTo->string());
81  $terms_identifiers = $this->forms_helper->transformArray($terms, 'identifier', $kindlyTo->int());
82 
83  foreach ($answers as $index => $answer) {
84  $filename = $terms_image_names[$index] ?? '';
85 
86  $upload_tmp_name = $this->request_data_collector->getUploadFilename(['terms', 'image'], $index);
87 
88  if (isset($uploads[$upload_tmp_name]) && $uploads[$upload_tmp_name]->isOk() &&
89  in_array($uploads[$upload_tmp_name]->getMimeType(), $allowed_mime_types)) {
90  $filename = '';
91  $name = $uploads[$upload_tmp_name]->getName();
92  if ($this->object->setImageFile(
93  $uploads[$upload_tmp_name]->getPath(),
94  $this->object->getEncryptedFilename($name)
95  )) {
96  $filename = $this->object->getEncryptedFilename($name);
97  }
98  }
99  // @PHP8-CR: There seems to be a bigger issue lingering here and won't suppress / "quickfix" this but
100  // postpone further analysis, eventually involving T&A TechSquad (see also remark in assMatchingQuestionGUI
101  $this->object->addTerm(
103  ilUtil::stripSlashes(htmlentities($answer)),
104  $filename,
105  $terms_identifiers[$index] ?? 0
106  )
107  );
108  }
109  }
110 
111  if ($this->request_data_collector->isset('definitions')) {
112  $definitions = $this->request_data_collector->raw('definitions');
113  $answers = $this->forms_helper->transformArray($definitions, 'answer', $kindlyTo->string());
114  $definitions_image_names = $this->forms_helper->transformArray($definitions, 'imagename', $kindlyTo->string());
115  $definitions_identifiers = $this->forms_helper->transformArray($definitions, 'identifier', $kindlyTo->int());
116 
117  foreach ($answers as $index => $answer) {
118  $filename = $definitions_image_names[$index] ?? '';
119 
120  $upload_tmp_name = $this->request_data_collector->getUploadFilename(['definitions', 'image'], $index);
121 
122  if (isset($uploads[$upload_tmp_name]) && $uploads[$upload_tmp_name]->isOk() &&
123  in_array($uploads[$upload_tmp_name]->getMimeType(), $allowed_mime_types)) {
124  $filename = '';
125  $name = $uploads[$upload_tmp_name]->getName();
126  if ($this->object->setImageFile(
127  $uploads[$upload_tmp_name]->getPath(),
128  $this->object->getEncryptedFilename($name)
129  )) {
130  $filename = $this->object->getEncryptedFilename($name);
131  }
132  }
133 
134  $this->object->addDefinition(
136  ilUtil::stripSlashes(htmlentities($answer)),
137  $filename,
138  $definitions_identifiers[$index] ?? 0
139  )
140  );
141  }
142  }
143 
144  if ($this->request_data_collector->isset('pairs')) {
145  $pairs = $this->request_data_collector->raw('pairs');
146  $points_of_pairs = $this->forms_helper->transformArray($pairs, 'points', $kindlyTo->float());
147  $pair_terms = $this->forms_helper->transformArray($pairs, 'term', $kindlyTo->int());
148  $pair_definitions = $this->forms_helper->transformArray($pairs, 'definition', $kindlyTo->int());
149 
150  foreach ($points_of_pairs as $index => $points) {
151  $term_id = $pair_terms[$index] ?? 0;
152  $definition_id = $pair_definitions[$index] ?? 0;
153  $this->object->addMatchingPair(
154  $this->object->getTermWithIdentifier($term_id),
155  $this->object->getDefinitionWithIdentifier($definition_id),
156  $points
157  );
158  }
159  }
160  }
161 
163  {
164  if (!$this->object->getSelfAssessmentEditingMode()) {
165  $this->object->setShuffle($this->request_data_collector->int('shuffle'));
166  $this->object->setShuffleMode($this->request_data_collector->int('shuffle'));
167  } else {
168  $this->object->setShuffle(1);
169  $this->object->setShuffleMode(1);
170  }
171  $this->object->setThumbGeometry($this->request_data_collector->int('thumb_geometry'));
172  $this->object->setMatchingMode($this->request_data_collector->string('matching_mode'));
173  }
174 
175  public function uploadterms(): void
176  {
178  $this->writePostData(true);
179  $this->editQuestion();
180  }
181 
182  public function removeimageterms(): void
183  {
185  $this->writePostData(true);
186  $this->object->removeTermImage($this->request_data_collector->getCmdIndex('removeimageterms'));
187  $this->editQuestion();
188  }
189 
190  public function uploaddefinitions(): void
191  {
193  $this->writePostData(true);
194  $this->editQuestion();
195  }
196 
197  public function removeimagedefinitions(): void
198  {
200  $this->writePostData(true);
201  $this->object->removeDefinitionImage($this->request_data_collector->getCmdIndex('removeimagedefinitions'));
202  $this->editQuestion();
203  }
204 
205  public function addterms(): void
206  {
208  $this->writePostData(true);
209  $add_terms = $this->request_data_collector->getCmdIndex('addterms');
210  $this->object->insertTerm($add_terms + 1);
211  $this->editQuestion();
212  }
213 
214  public function removeterms(): void
215  {
217  $this->writePostData(true);
218  $this->object->deleteTerm($this->request_data_collector->getCmdIndex('removeterms'));
219  $this->editQuestion();
220  }
221 
222  public function adddefinitions(): void
223  {
225  $this->writePostData(true);
226  $this->object->insertDefinition($this->request_data_collector->getCmdIndex('adddefinitions') + 1);
227  $this->editQuestion();
228  }
229 
230  public function removedefinitions(): void
231  {
233  $this->writePostData(true);
234  $this->object->deleteDefinition($this->request_data_collector->getCmdIndex('removedefinitions'));
235  $this->editQuestion();
236  }
237 
238  public function addpairs(): void
239  {
241  $this->writePostData(true);
242  $this->object->insertMatchingPair($this->request_data_collector->getCmdIndex('addpairs') + 1);
243  $this->editQuestion();
244  }
245 
246  public function removepairs(): void
247  {
249  $this->writePostData(true);
250  $this->object->deleteMatchingPair($this->request_data_collector->getCmdIndex('removepairs'));
251  $this->editQuestion();
252  }
253 
254  public function editQuestion(
255  bool $checkonly = false,
256  ?bool $is_save_cmd = null
257  ): bool {
258  $save = $is_save_cmd ?? $this->isSaveCommand();
259 
260  $form = new ilPropertyFormGUI();
261  $this->editForm = $form;
262 
263  $form->setFormAction($this->ctrl->getFormAction($this));
264  $form->setTitle($this->outQuestionType());
265  $form->setMultipart(true);
266  $form->setTableWidth("100%");
267  $form->setId("matching");
268 
269  $this->addBasicQuestionFormProperties($form);
270  $this->populateQuestionSpecificFormPart($form);
271  $this->populateAnswerSpecificFormPart($form);
272  $this->populateTaxonomyFormSection($form);
273  $this->addQuestionFormCommandButtons($form);
274 
275  $errors = false;
276  if ($save) {
277  $form->setValuesByPost();
278  $errors = !$form->checkInput();
279  $form->setValuesByPost(); // again, because checkInput now performs the whole stripSlashes handling and we need this if we don't want to have duplication of backslashes
280  if (!$errors && !$this->isValidTermAndDefinitionAmount($form) && !$this->object->getSelfAssessmentEditingMode()) {
281  $errors = true;
282  $terms = $form->getItemByPostVar('terms');
283  $terms->setAlert($this->lng->txt("msg_number_of_terms_too_low"));
284  $this->tpl->setOnScreenMessage('failure', $this->lng->txt('form_input_not_valid'));
285  }
286  if ($errors) {
287  $checkonly = false;
288  }
289  }
290 
291  if (!$checkonly) {
292  $this->renderEditForm($form);
293  }
294  return $errors;
295  }
296 
297  private function isDefImgUploadCommand(): bool
298  {
299  return $this->ctrl->getCmd() == 'uploaddefinitions';
300  }
301 
302  private function isTermImgUploadCommand(): bool
303  {
304  return $this->ctrl->getCmd() == 'uploadterms';
305  }
306 
314  private function isValidTermAndDefinitionAmount(ilPropertyFormGUI $form): bool
315  {
316  $matchingMode = $form->getItemByPostVar('matching_mode')->getValue();
317 
318  if ($matchingMode == assMatchingQuestion::MATCHING_MODE_N_ON_N) {
319  return true;
320  }
321 
322  $numTerms = count($form->getItemByPostVar('terms')->getValues());
323  $numDefinitions = count($form->getItemByPostVar('definitions')->getValues());
324 
325  if ($numTerms >= $numDefinitions) {
326  return true;
327  }
328 
329  return false;
330  }
331 
333  {
334  $definitions = new ilMatchingWizardInputGUI($this->lng->txt("definitions"), "definitions");
335  if ($this->object->getSelfAssessmentEditingMode()) {
336  $definitions->setHideImages(true);
337  }
338 
339  $stripHtmlEntitesFromValues = function (assAnswerMatchingTerm $value) {
340  return $value->withText(html_entity_decode($value->getText()));
341  };
342 
343  $definitions->setRequired(true);
344  $definitions->setQuestionObject($this->object);
345  $definitions->setTextName($this->lng->txt('definition_text'));
346  $definitions->setImageName($this->lng->txt('definition_image'));
347  if (!count($this->object->getDefinitions())) {
348  $this->object->addDefinition(new assAnswerMatchingDefinition());
349  }
350  $definitionvalues = array_map($stripHtmlEntitesFromValues, $this->object->getDefinitions());
351  $definitions->setValues($definitionvalues);
352  if ($this->isDefImgUploadCommand()) {
353  $definitions->checkInput();
354  }
355  $form->addItem($definitions);
356 
357  $terms = new ilMatchingWizardInputGUI($this->lng->txt("terms"), "terms");
358  if ($this->object->getSelfAssessmentEditingMode()) {
359  $terms->setHideImages(true);
360  }
361  $terms->setRequired(true);
362  $terms->setQuestionObject($this->object);
363  $terms->setTextName($this->lng->txt('term_text'));
364  $terms->setImageName($this->lng->txt('term_image'));
365 
366  if (0 === count($this->object->getTerms())) {
367  // @PHP8-CR: If you look above, how $this->object->addDefinition does in fact take an object, I take this
368  // issue as an indicator for a bigger issue and won't suppress / "quickfix" this but postpone further
369  // analysis, eventually involving T&A TechSquad
370  $this->object->addTerm(new assAnswerMatchingTerm());
371  }
372  $termvalues = array_map($stripHtmlEntitesFromValues, $this->object->getTerms());
373  $terms->setValues($termvalues);
374  if ($this->isTermImgUploadCommand()) {
375  $terms->checkInput();
376  }
377  $form->addItem($terms);
378 
379  $pairs = new ilMatchingPairWizardInputGUI($this->lng->txt('matching_pairs'), 'pairs');
380  $pairs->setRequired(true);
381  $pairs->setTerms($this->object->getTerms());
382  $pairs->setDefinitions($this->object->getDefinitions());
383  if (count($this->object->getMatchingPairs()) == 0) {
384  $this->object->addMatchingPair($termvalues[0], $definitionvalues[0], 0);
385  //$this->object->addMatchingPair(new assAnswerMatchingPair($termvalues[0], $definitionvalues[0], 0));
386  }
387  $pairs->setPairs($this->object->getMatchingPairs());
388  $form->addItem($pairs);
389 
390  return $form;
391  }
392 
394  {
395  // Edit mode
396  $hidden = new ilHiddenInputGUI("matching_type");
397  $hidden->setValue('');
398  $form->addItem($hidden);
399 
400  if (!$this->object->getSelfAssessmentEditingMode()) {
401  // shuffle
402  $shuffle = new ilSelectInputGUI($this->lng->txt("shuffle_answers"), "shuffle");
403  $shuffle_options = [
404  0 => $this->lng->txt("no"),
405  1 => $this->lng->txt("matching_shuffle_terms_definitions"),
406  2 => $this->lng->txt("matching_shuffle_terms"),
407  3 => $this->lng->txt("matching_shuffle_definitions")
408  ];
409  $shuffle->setOptions($shuffle_options);
410  $shuffle->setValue($this->object->getShuffleMode());
411  $shuffle->setRequired(false);
412  $form->addItem($shuffle);
413 
414  $geometry = new ilNumberInputGUI($this->lng->txt('thumb_size'), 'thumb_geometry');
415  $geometry->setValue((string) $this->object->getThumbGeometry());
416  $geometry->setRequired(true);
417  $geometry->setMaxLength(6);
418  $geometry->setMinValue($this->object->getMinimumThumbSize());
419  $geometry->setMaxValue($this->object->getMaximumThumbSize());
420  $geometry->setSize(6);
421  $geometry->setInfo($this->lng->txt('thumb_size_info'));
422  $form->addItem($geometry);
423  }
424 
425  // Matching Mode
426  $mode = new ilRadioGroupInputGUI($this->lng->txt('qpl_qst_inp_matching_mode'), 'matching_mode');
427  $mode->setRequired(true);
428 
429  $modeONEonONE = new ilRadioOption(
430  $this->lng->txt('qpl_qst_inp_matching_mode_one_on_one'),
432  );
433  $mode->addOption($modeONEonONE);
434 
435  $modeALLonALL = new ilRadioOption(
436  $this->lng->txt('qpl_qst_inp_matching_mode_all_on_all'),
438  );
439  $mode->addOption($modeALLonALL);
440 
441  $mode->setValue($this->object->getMatchingMode());
442 
443  $form->addItem($mode);
444  return $form;
445  }
446 
447  public function getSolutionOutput(
448  int $active_id,
449  ?int $pass = null,
450  bool $graphical_output = false,
451  bool $result_output = false,
452  bool $show_question_only = true,
453  bool $show_feedback = false,
454  bool $show_correct_solution = false,
455  bool $show_manual_scoring = false,
456  bool $show_question_text = true,
457  bool $show_inline_feedback = true
458  ): string {
459  $solutions = [];
460  if (($active_id > 0) && (!$show_correct_solution)) {
461  $solutions = $this->object->getSolutionValues($active_id, $pass);
462  } else {
463  foreach ($this->object->getMaximumScoringMatchingPairs() as $pair) {
464  $solutions[] = [
465  'value1' => $pair->getTerm()->getIdentifier(),
466  'value2' => $pair->getDefinition()->getIdentifier(),
467  'points' => $pair->getPoints()
468  ];
469  }
470  }
471 
472  return $this->renderSolutionOutput(
473  $solutions,
474  $active_id,
475  $pass,
476  $graphical_output,
477  $result_output,
478  $show_question_only,
479  $show_feedback,
480  $show_correct_solution,
481  $show_manual_scoring,
482  $show_question_text,
483  false,
484  $show_inline_feedback,
485  );
486  }
487 
488  public function renderSolutionOutput(
489  mixed $user_solutions,
490  int $active_id,
491  ?int $pass,
492  bool $graphical_output = false,
493  bool $result_output = false,
494  bool $show_question_only = true,
495  bool $show_feedback = false,
496  bool $show_correct_solution = false,
497  bool $show_manual_scoring = false,
498  bool $show_question_text = true,
499  bool $show_autosave_title = false,
500  bool $show_inline_feedback = false,
501  ): ?string {
502  $template = new ilTemplate('tpl.il_as_qpl_matching_output_solution.html', true, true, 'components/ILIAS/TestQuestionPool');
503  $solutiontemplate = new ilTemplate('tpl.il_as_tst_solution_output.html', true, true, 'components/ILIAS/TestQuestionPool');
504  $i = 0;
505 
506  foreach ($user_solutions as $solution) {
507  $definition = $this->object->getDefinitionWithIdentifier($solution['value2']);
508  $term = $this->object->getTermWithIdentifier($solution['value1']);
509  $points = $solution['points'];
510 
511  if (is_object($definition)) {
512  if ($definition->getPicture() !== '') {
513  if ($definition->getText() !== '') {
514  $template->setCurrentBlock('definition_image_text');
515  $template->setVariable(
516  'TEXT_DEFINITION',
517  ilLegacyFormElementsUtil::prepareFormOutput($definition->getText())
518  );
519  $template->parseCurrentBlock();
520  }
521 
522  $answerImageSrc = ilWACSignedPath::signFile(
523  $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $definition->getPicture()
524  );
525 
526  $template->setCurrentBlock('definition_image');
527  $template->setVariable('ANSWER_IMAGE_URL', $answerImageSrc);
528  $template->setVariable(
529  'ANSWER_IMAGE_ALT',
530  (strlen($definition->getText())) ? ilLegacyFormElementsUtil::prepareFormOutput(
531  $definition->getText()
532  ) : ilLegacyFormElementsUtil::prepareFormOutput($definition->getPicture())
533  );
534  $template->setVariable(
535  'ANSWER_IMAGE_TITLE',
536  (strlen($definition->getText())) ? ilLegacyFormElementsUtil::prepareFormOutput(
537  $definition->getText()
538  ) : ilLegacyFormElementsUtil::prepareFormOutput($definition->getPicture())
539  );
540  $template->setVariable('URL_PREVIEW', $this->object->getImagePathWeb() . $definition->getPicture());
541  $template->setVariable("TEXT_PREVIEW", $this->lng->txt('preview'));
542  $template->setVariable("IMG_PREVIEW", ilUtil::getImagePath('media/enlarge.svg'));
543  $template->parseCurrentBlock();
544  } else {
545  $template->setCurrentBlock('definition_text');
546  $template->setVariable("DEFINITION", ilLegacyFormElementsUtil::prepareTextareaOutput($definition->getText(), true));
547  $template->parseCurrentBlock();
548  }
549  }
550  if ($term !== null) {
551  if (strlen($term->getPicture())) {
552  if (strlen($term->getText())) {
553  $template->setCurrentBlock('term_image_text');
554  $template->setVariable("TEXT_TERM", ilLegacyFormElementsUtil::prepareFormOutput($term->getText()));
555  $template->parseCurrentBlock();
556  }
557 
558  $answerImageSrc = ilWACSignedPath::signFile(
559  $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $term->getPicture()
560  );
561 
562  $template->setCurrentBlock('term_image');
563  $template->setVariable('ANSWER_IMAGE_URL', $answerImageSrc);
564  $template->setVariable(
565  'ANSWER_IMAGE_ALT',
566  (strlen($term->getText())) ? ilLegacyFormElementsUtil::prepareFormOutput(
567  $term->getText()
568  ) : ilLegacyFormElementsUtil::prepareFormOutput($term->getPicture())
569  );
570  $template->setVariable(
571  'ANSWER_IMAGE_TITLE',
572  (strlen($term->getText())) ? ilLegacyFormElementsUtil::prepareFormOutput(
573  $term->getText()
574  ) : ilLegacyFormElementsUtil::prepareFormOutput($term->getPicture())
575  );
576  $template->setVariable('URL_PREVIEW', $this->object->getImagePathWeb() . $term->getPicture());
577  $template->setVariable("TEXT_PREVIEW", $this->lng->txt('preview'));
578  $template->setVariable("IMG_PREVIEW", ilUtil::getImagePath('media/enlarge.svg'));
579  $template->parseCurrentBlock();
580  } else {
581  $template->setCurrentBlock('term_text');
582  $template->setVariable("TERM", ilLegacyFormElementsUtil::prepareTextareaOutput($term->getText(), true));
583  $template->parseCurrentBlock();
584  }
585  $i++;
586  }
587  if (($active_id > 0) && (!$show_correct_solution)) {
588  if ($graphical_output) {
589  // output of ok/not ok icons for user entered solutions
590  $ok = false;
591  foreach ($this->object->getMatchingPairs() as $pair) {
592  if ($this->isCorrectMatching($pair, $definition, $term)) {
593  $ok = true;
594  }
595  }
596 
597  $correctness_icon = $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_NOT_OK);
598  if ($ok) {
599  $correctness_icon = $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_OK);
600  }
601  $template->setCurrentBlock('icon_ok');
602  $template->setVariable('ICON_OK', $correctness_icon);
603  $template->parseCurrentBlock();
604  }
605  }
606 
607  if ($result_output) {
608  $resulttext = ($points == 1) ? '(%s ' . $this->lng->txt('point') . ')' : '(%s ' . $this->lng->txt('points') . ')';
609  $template->setCurrentBlock('result_output');
610  $template->setVariable('RESULT_OUTPUT', sprintf($resulttext, $points));
611  $template->parseCurrentBlock();
612  }
613 
614  $template->setCurrentBlock('row');
615  $template->setVariable('TEXT_MATCHES', $this->lng->txt('matches'));
616  $template->parseCurrentBlock();
617  }
618 
619  if ($show_question_text == true) {
620  $template->setVariable('QUESTIONTEXT', $this->object->getQuestionForHTMLOutput());
621  }
622 
623  $questionoutput = $template->get();
624 
625  $feedback = '';
626  if ($show_feedback) {
627  if (!$this->isTestPresentationContext()) {
628  $fb = $this->getGenericFeedbackOutput((int) $active_id, $pass);
629  $feedback .= strlen($fb) ? $fb : '';
630  }
631 
632  $fb = $this->getSpecificFeedbackOutput([]);
633  $feedback .= strlen($fb) ? $fb : '';
634  }
635  if (strlen($feedback)) {
636  $cssClass = (
637  $this->hasCorrectSolution($active_id, $pass) ?
639  );
640 
641  $solutiontemplate->setVariable('ILC_FB_CSS_CLASS', $cssClass);
642  $solutiontemplate->setVariable('FEEDBACK', ilLegacyFormElementsUtil::prepareTextareaOutput($feedback, true));
643  }
644 
645  $solutiontemplate->setVariable('SOLUTION_OUTPUT', $questionoutput);
646 
647  $solutionoutput = $solutiontemplate->get();
648  if (!$show_question_only) {
649  // get page object output
650  $solutionoutput = $this->getILIASPage($solutionoutput);
651  }
652  return $solutionoutput;
653  }
654 
655  public function getPreview(
656  bool $show_question_only = false,
657  bool $show_inline_feedback = false
658  ): string {
659  $template = new ilTemplate('tpl.il_as_qpl_matching_output.html', true, true, 'components/ILIAS/TestQuestionPool');
660  $this->initializePlayerJS();
661 
662  $solutions = $this->getPreviewSession()?->getParticipantsSolution() ?? [];
663 
664  // shuffle output
665  $terms = $this->object->getTerms();
666  $definitions = $this->object->getDefinitions();
667  switch ($this->object->getShuffleMode()) {
668  case 1:
669  $terms = $this->object->getShuffler()->transform($terms);
670  $definitions = $this->object->getShuffler()->transform(
671  $this->object->getShuffler()->transform($definitions)
672  );
673  break;
674  case 2:
675  $terms = $this->object->getShuffler()->transform($terms);
676  break;
677  case 3:
678  $definitions = $this->object->getShuffler()->transform($definitions);
679  break;
680  }
681 
682  foreach ($definitions as $definition) {
683  $terms = $this->populateDefinition($template, $definition, $solutions, $terms);
684  $template->setCurrentBlock('droparea');
685  $template->setVariable('ID_DROPAREA', $definition->getIdentifier());
686  $template->setVariable('QUESTION_ID', $this->object->getId());
687  $template->parseCurrentBlock();
688  }
689 
690  $template->setVariable(
691  'TERMS_PRESENTATION_SOURCE',
692  array_reduce(
693  $terms,
694  fn(string $c, assAnswerMatchingTerm $v) => $c . $this->buildTermHtml($v),
695  ''
696  )
697  );
698 
699  $template->setVariable('QUESTIONTEXT', $this->object->getQuestionForHTMLOutput());
700 
701  $questionoutput = $template->get();
702 
703  if (!$show_question_only) {
704  // get page object output
705  $questionoutput = $this->getILIASPage($questionoutput);
706  }
707 
708  return $questionoutput;
709  }
710 
711  private function populateDefinition(
712  ilTemplate $template,
713  assAnswerMatchingDefinition $definition,
714  array $solutions,
715  array $terms
716  ): array {
717  if ($definition->getPicture() !== '') {
718  $template->setCurrentBlock('definition_picture');
719  $template->setVariable('DEFINITION_ID', $definition->getIdentifier());
720  $template->setVariable('IMAGE_HREF', $this->object->getImagePathWeb() . $definition->getPicture());
721  $thumbweb = $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $definition->getPicture();
722  $thumb = $this->object->getImagePath() . $this->object->getThumbPrefix() . $definition->getPicture();
723  if (!file_exists($thumb)) {
724  $this->object->rebuildThumbnails();
725  }
726  $template->setVariable('THUMBNAIL_HREF', $thumbweb);
727  $template->setVariable('THUMB_ALT', $this->lng->txt('image'));
728  $template->setVariable('THUMB_TITLE', $this->lng->txt('image'));
729  $template->setVariable('TEXT_DEFINITION', (strlen($definition->getText())) ? ilLegacyFormElementsUtil::prepareTextareaOutput($definition->getText(), true, true) : '');
730  $template->setVariable('TEXT_PREVIEW', $this->lng->txt('preview'));
731  $template->setVariable('IMG_PREVIEW', ilUtil::getImagePath('media/enlarge.svg'));
732  $template->parseCurrentBlock();
733  } else {
734  $template->setCurrentBlock('definition_text');
735  $template->setVariable('DEFINITION', ilLegacyFormElementsUtil::prepareTextareaOutput($definition->getText(), true, true));
736  $template->parseCurrentBlock();
737  }
738 
739  if ($solutions === []
740  || !array_key_exists($definition->getIdentifier(), $solutions)) {
741  $template->setVariable('ASSIGNED_TERMS', json_encode([]));
742  return $terms;
743  }
744 
745  return $this->populateAssignedTerms($template, $definition->getIdentifier(), $solutions[$definition->getIdentifier()], $terms);
746  }
747 
751  private function populateAssignedTerms(
752  ilTemplate $definition_template,
753  int $definition_id,
754  array $assigned_term_ids,
755  array $available_terms
756  ): array {
757  $definition_template->setVariable('ASSIGNED_TERMS', json_encode($assigned_term_ids));
758  $definition_template->setVariable(
759  'TERMS_PRESENTATION_ASSIGNED',
760  array_reduce(
761  $assigned_term_ids,
762  function (string $c, int $v) use ($definition_id, &$available_terms) {
763  $key = $this->getArrayKeyForTermId($v, $available_terms);
764  if ($key === null) {
765  return $c;
766  }
767  $c .= $this->buildTermHtml($available_terms[$key], $definition_id);
768  if ($this->object->getMatchingMode() === assMatchingQuestion::MATCHING_MODE_1_ON_1) {
769  unset($available_terms[$key]);
770  }
771  return $c;
772  },
773  ''
774  )
775  );
776 
777  return $available_terms;
778  }
779 
780  private function getArrayKeyForTermId(int $term_id, array $terms): ?int
781  {
782  foreach ($terms as $key => $term) {
783  if ($term->getIdentifier() === $term_id) {
784  return $key;
785  }
786  }
787  return null;
788  }
789 
790  private function buildTermHtml(assAnswerMatchingTerm $term, ?int $definition_id = null): string
791  {
792  $template = new ilTemplate('tpl.il_as_qpl_matching_term_output.html', true, true, 'components/ILIAS/TestQuestionPool');
793 
794  $template->setVariable('ID_DRAGGABLE', $term->getIdentifier());
795 
796  if ($definition_id !== null) {
797  $template->setCurrentBlock('definition_id');
798  $template->setVariable('IID_DROPAREA', $definition_id);
799  $template->parseCurrentBlock();
800  }
801 
802  if ($term->getPicture() === '') {
803  $template->setCurrentBlock('term_text');
804  $template->setVariable('TERM_TEXT', ilLegacyFormElementsUtil::prepareTextareaOutput($term->getText(), true, true));
805  $template->parseCurrentBlock();
806  return $template->get();
807  }
808 
809  $template->setCurrentBlock('term_picture');
810  $template->setVariable('TERM_ID', $term->getIdentifier());
811  $template->setVariable('IMAGE_HREF', $this->object->getImagePathWeb() . $term->getPicture());
812  $thumbweb = $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $term->getPicture();
813  $thumb = $this->object->getImagePath() . $this->object->getThumbPrefix() . $term->getPicture();
814  if (!file_exists($thumb)) {
815  $this->object->rebuildThumbnails();
816  }
817  $template->setVariable('THUMBNAIL_HREF', $thumbweb);
818  $template->setVariable('THUMB_ALT', $this->lng->txt('image'));
819  $template->setVariable('THUMB_TITLE', $this->lng->txt('image'));
820  $template->setVariable('TEXT_PREVIEW', $this->lng->txt('preview'));
821  $template->setVariable('TEXT_TERM', $term->getText() !== ''
823  : '');
824  $template->setVariable('IMG_PREVIEW', ilUtil::getImagePath('media/enlarge.svg'));
825  $template->parseCurrentBlock();
826  return $template->get();
827  }
828 
829  private function buildSolutionsArray(int $active_id, int $attempt, array|bool $user_post_solutions): array
830  {
831  if ($active_id === 0) {
832  return [];
833  }
834  if ($user_post_solutions !== false) {
835  return $user_post_solutions['matching'];
836  }
837 
838  return array_reduce(
839  $this->object->getTestOutputSolutions($active_id, $attempt),
840  static function (array $c, array $v): array {
841  if (!array_key_exists($v['value2'], $c)) {
842  $c[$v['value2']] = [$v['value1']];
843  return $c;
844  }
845  $c[$v['value2']][] = $v['value1'];
846  return $c;
847  },
848  []
849  );
850  }
851 
857  protected function sortDefinitionsBySolution(array $solutions, array $definitions): array
858  {
859  $neworder = [];
860  $handled_definitions = [];
861  foreach (array_keys($solutions) as $definition_id) {
862  $neworder[] = $this->object->getDefinitionWithIdentifier($definition_id);
863  $handled_definitions[$definition_id] = $definition_id;
864  }
865 
866  foreach ($definitions as $definition) {
867  if (!isset($handled_definitions[$definition->getIdentifier()])) {
868  $neworder[] = $definition;
869  }
870  }
871 
872  return $neworder;
873  }
874 
875  public function getTestOutput(
876  int $active_id,
877  int $attempt,
878  bool $is_question_postponed = false,
879  array|bool $user_post_solutions = false,
880  bool $show_specific_inline_feedback = false
881  ): string {
882  $template = new ilTemplate('tpl.il_as_qpl_matching_output.html', true, true, 'components/ILIAS/TestQuestionPool');
883  $this->initializePlayerJS();
884 
885  $solutions = $this->buildSolutionsArray($active_id, $attempt, $user_post_solutions);
886  $terms = $this->object->getTerms();
887  $definitions = $this->object->getDefinitions();
888  switch ($this->object->getShuffleMode()) {
889  case 1:
890  $terms = $this->object->getShuffler()->transform($terms);
891  if ($solutions !== []) {
892  $definitions = $this->sortDefinitionsBySolution($solutions, $definitions);
893  } else {
894  $definitions = $this->object->getShuffler()->transform(
895  $this->object->getShuffler()->transform($definitions)
896  );
897  }
898  break;
899  case 2:
900  $terms = $this->object->getShuffler()->transform($terms);
901  break;
902  case 3:
903  if ($solutions !== []) {
904  $definitions = $this->sortDefinitionsBySolution($solutions, $definitions);
905  } else {
906  $definitions = $this->object->getShuffler()->transform($definitions);
907  }
908  break;
909  }
910 
911  foreach ($definitions as $definition) {
912  $terms = $this->populateDefinition($template, $definition, $solutions, $terms);
913  $template->setCurrentBlock('droparea');
914  $template->setVariable('ID_DROPAREA', $definition->getIdentifier());
915  $template->setVariable('QUESTION_ID', $this->object->getId());
916  $template->parseCurrentBlock();
917  }
918 
919  $template->setVariable(
920  'TERMS_PRESENTATION_SOURCE',
921  array_reduce(
922  $terms,
923  fn(string $c, assAnswerMatchingTerm $v) => $c . $this->buildTermHtml($v),
924  ''
925  )
926  );
927 
928  $template->setVariable('QUESTIONTEXT', $this->object->getQuestionForHTMLOutput());
929 
930  return $this->outQuestionPage('', $is_question_postponed, $active_id, $template->get());
931  }
932 
936  public function checkInput(): bool
937  {
938  $title = $this->request_data_collector->string('title');
939  $author = $this->request_data_collector->string('author');
940  $question = $this->request_data_collector->string('question');
941 
942  return !empty($title) && !empty($author) && !empty($question);
943  }
944 
945  public function getSpecificFeedbackOutput(array $userSolution): string
946  {
947  $matches = array_values($this->object->matchingpairs);
948 
949  if (!$this->object->feedbackOBJ->specificAnswerFeedbackExists()) {
950  return '';
951  }
952 
953  $feedback = '<table class="test_specific_feedback"><tbody>';
954 
955  foreach ($matches as $idx => $ans) {
956  if (!isset($userSolution[$ans->getDefinition()->getIdentifier()])) {
957  continue;
958  }
959 
960  if (!is_array($userSolution[$ans->getDefinition()->getIdentifier()])) {
961  continue;
962  }
963 
964  if (!in_array($ans->getTerm()->getIdentifier(), $userSolution[$ans->getDefinition()->getIdentifier()])) {
965  continue;
966  }
967 
968  $fb = $this->object->feedbackOBJ->getSpecificAnswerFeedbackTestPresentation(
969  $this->object->getId(),
970  0,
971  $idx
972  );
973  $feedback .= "<tr><td>\"{$ans->getDefinition()->getText()}\" {$this->lng->txt('matches')} ";
974  $feedback .= "\"{$ans->getTerm()->getText()}\"</td><td>{$fb}</td></tr>";
975  }
976 
977  $feedback .= '</tbody></table>';
978  return ilLegacyFormElementsUtil::prepareTextareaOutput($feedback, true);
979  }
980 
991  {
992  return [];
993  }
994 
1005  {
1006  return [];
1007  }
1008 
1009  private function isCorrectMatching($pair, $definition, $term): bool
1010  {
1011  if (!($pair->getPoints() > 0)) {
1012  return false;
1013  }
1014 
1015  if (!is_object($term)) {
1016  return false;
1017  }
1018 
1019  if ($pair->getDefinition()->getIdentifier() != $definition->getIdentifier()) {
1020  return false;
1021  }
1022 
1023  if ($pair->getTerm()->getIdentifier() != $term->getIdentifier()) {
1024  return false;
1025  }
1026 
1027  return true;
1028  }
1029 
1030  protected function getAnswerStatisticImageHtml($picture): string
1031  {
1032  $thumbweb = $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $picture;
1033  return '<img src="' . $thumbweb . '" alt="' . $picture . '" title="' . $picture . '"/>';
1034  }
1035 
1036  protected function getAnswerStatisticMatchingElemHtml($elem): string
1037  {
1038  $html = '';
1039 
1040  if (strlen($elem->getText())) {
1041  $html .= $elem->getText();
1042  }
1043 
1044  if (strlen($elem->getPicture())) {
1045  $html .= $this->getAnswerStatisticImageHtml($elem->getPicture());
1046  }
1047 
1048  return $html;
1049  }
1050 
1051  public function getAnswersFrequency($relevantAnswers, $questionIndex): array
1052  {
1053  $answersByActiveAndPass = [];
1054 
1055  foreach ($relevantAnswers as $row) {
1056  $key = $row['active_fi'] . ':' . $row['pass'];
1057 
1058  if (!isset($answersByActiveAndPass[$key])) {
1059  $answersByActiveAndPass[$key] = [];
1060  }
1061 
1062  $answersByActiveAndPass[$key][$row['value1']] = $row['value2'];
1063  }
1064 
1065  $answers = [];
1066 
1067  foreach ($answersByActiveAndPass as $key => $matchingPairs) {
1068  foreach ($matchingPairs as $termId => $defId) {
1069  $hash = md5($termId . ':' . $defId);
1070 
1071  if (!isset($answers[$hash])) {
1072  $termHtml = $this->getAnswerStatisticMatchingElemHtml(
1073  $this->object->getTermWithIdentifier($termId)
1074  );
1075 
1076  $defHtml = $this->getAnswerStatisticMatchingElemHtml(
1077  $this->object->getDefinitionWithIdentifier($defId)
1078  );
1079 
1080  $answers[$hash] = [
1081  'answer' => $termHtml . $defHtml,
1082  'term' => $termHtml,
1083  'definition' => $defHtml,
1084  'frequency' => 0
1085  ];
1086  }
1087 
1088  $answers[$hash]['frequency']++;
1089  }
1090  }
1091 
1092  return $answers;
1093  }
1094 
1102  public function getAnswerFrequencyTableGUI($parentGui, $parentCmd, $relevantAnswers, $questionIndex): ilAnswerFrequencyStatisticTableGUI
1103  {
1104  $table = new ilMatchingQuestionAnswerFreqStatTableGUI($parentGui, $parentCmd, $this->object);
1105  $table->setQuestionIndex($questionIndex);
1106  $table->setData($this->getAnswersFrequency($relevantAnswers, $questionIndex));
1107  $table->initColumns();
1108 
1109  return $table;
1110  }
1111 
1113  {
1114  $pairs = new ilAssMatchingPairCorrectionsInputGUI($this->lng->txt('matching_pairs'), 'pairs');
1115  $pairs->setRequired(true);
1116  $pairs->setTerms($this->object->getTerms());
1117  $pairs->setDefinitions($this->object->getDefinitions());
1118  $pairs->setPairs($this->object->getMatchingPairs());
1119  $pairs->setThumbsWebPathWithPrefix($this->object->getImagePathWeb() . $this->object->getThumbPrefix());
1120  $form->addItem($pairs);
1121  }
1122 
1127  {
1128  $pairs = $this->object->getMatchingPairs();
1129  $nu_pairs = [];
1130 
1131  if ($this->request_data_collector->isset('pairs')) {
1132  $points_of_pairs = $this->request_data_collector->raw('pairs')['points'];
1133  $pair_terms = explode(',', $this->request_data_collector->raw('pairs')['term_id']);
1134  $pair_definitions = explode(',', $this->request_data_collector->raw('pairs')['definition_id']);
1135  $values = [];
1136  foreach ($points_of_pairs as $idx => $points) {
1137  $k = implode('.', [$pair_terms[$idx], $pair_definitions[$idx]]);
1138  $values[$k] = (float) str_replace(',', '.', $points);
1139  }
1140 
1141  foreach ($pairs as $idx => $pair) {
1142  $id = implode('.', [
1143  $pair->getTerm()->getIdentifier(),
1144  $pair->getDefinition()->getIdentifier()
1145  ]);
1146  $nu_pairs[$id] = $pair->withPoints($values[$id]);
1147  }
1148 
1149  $this->object = $this->object->withMatchingPairs($nu_pairs);
1150  }
1151  }
1152 
1153  private function initializePlayerJS(): void
1154  {
1155  $this->tpl->addJavaScript('assets/js/matching.js');
1156  $this->tpl->addOnLoadCode(
1157  'il.test.matching.init('
1158  . "document.querySelector('#ilMatchingQuestionContainer_{$this->object->getId()}'),"
1159  . "'{$this->object->getMatchingMode()}');"
1160  );
1161  }
1162 }
parseCurrentBlock(string $part=ilGlobalTemplateInterface::DEFAULT_BLOCK)
Class for matching question terms.
hasCorrectSolution($activeId, $passIndex)
This class represents an option in a radio group.
getArrayKeyForTermId(int $term_id, array $terms)
generateCorrectnessIconsForCorrectness(int $correctness)
This class represents a single choice wizard property in a property form.
This class represents a selection list property in a property form.
setHideImages($a_hide)
Set hide images.
saveCorrectionsFormProperties(ilPropertyFormGUI $form)
getItemByPostVar(string $a_post_var)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static stripSlashes(string $a_str, bool $a_strip_html=true, string $a_allow="")
addBasicQuestionFormProperties(ilPropertyFormGUI $form)
isValidTermAndDefinitionAmount(ilPropertyFormGUI $form)
for mode 1:1 terms count must not be less than definitions count for mode n:n this limitation is canc...
populateCorrectionsFormProperties(ilPropertyFormGUI $form)
setOptions(array $a_options)
populateDefinition(ilTemplate $template, assAnswerMatchingDefinition $definition, array $solutions, array $terms)
static prepareFormOutput($a_str, bool $a_strip=false)
$c
Definition: deliver.php:25
Matching question GUI representation.
populateTaxonomyFormSection(ilPropertyFormGUI $form)
addQuestionFormCommandButtons(ilPropertyFormGUI $form)
Class for matching questions.
writeAnswerSpecificPostData(ilPropertyFormGUI $form)
Extracts the answer specific values from the request and applies them to the data object...
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
buildTermHtml(assAnswerMatchingTerm $term, ?int $definition_id=null)
populateQuestionSpecificFormPart(\ilPropertyFormGUI $form)
This class represents a hidden form property in a property form.
This class represents a property in a property form.
setVariable($variable, $value='')
Sets a variable value.
Definition: IT.php:544
setErrorMessage(string $errormessage)
This class represents a number property in a property form.
setValue(?string $a_value)
writePostData(bool $always=false)
{}
writeQuestionSpecificPostData(ilPropertyFormGUI $form)
Extracts the question specific values from the request and applies them to the data object...
setValue(string $a_value)
editQuestion(bool $checkonly=false, ?bool $is_save_cmd=null)
static getImagePath(string $image_name, string $module_path="", string $mode="output", bool $offline=false)
get image path (for images located in a template directory)
isCorrectMatching($pair, $definition, $term)
getAnswerFrequencyTableGUI($parentGui, $parentCmd, $relevantAnswers, $questionIndex)
sortDefinitionsBySolution(array $solutions, array $definitions)
setRequired(bool $a_required)
populateAnswerSpecificFormPart(\ilPropertyFormGUI $form)
getTestOutput(int $active_id, int $attempt, bool $is_question_postponed=false, array|bool $user_post_solutions=false, bool $show_specific_inline_feedback=false)
$filename
Definition: buildRTE.php:78
buildSolutionsArray(int $active_id, int $attempt, array|bool $user_post_solutions)
getPreview(bool $show_question_only=false, bool $show_inline_feedback=false)
setCurrentBlock(string $part=ilGlobalTemplateInterface::DEFAULT_BLOCK)
getSpecificFeedbackOutput(array $userSolution)
getILIASPage(string $html="")
Returns the ILIAS Page around a question.
outQuestionPage($a_temp_var, $a_postponed=false, $active_id="", $html="", $inlineFeedbackEnabled=false)
renderSolutionOutput(mixed $user_solutions, int $active_id, ?int $pass, bool $graphical_output=false, bool $result_output=false, bool $show_question_only=true, bool $show_feedback=false, bool $show_correct_solution=false, bool $show_manual_scoring=false, bool $show_question_text=true, bool $show_autosave_title=false, bool $show_inline_feedback=false,)
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
populateAssignedTerms(ilTemplate $definition_template, int $definition_id, array $assigned_term_ids, array $available_terms)
__construct(Container $dic, ilPlugin $plugin)
getAnswersFrequency($relevantAnswers, $questionIndex)
static signFile(string $path_to_file)
Class for matching question definitions.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getAfterParticipationSuppressionQuestionPostVars()
Returns a list of postvars which will be suppressed in the form output when used in scoring adjustmen...
getAfterParticipationSuppressionAnswerPostVars()
Returns a list of postvars which will be suppressed in the form output when used in scoring adjustmen...
getSolutionOutput(int $active_id, ?int $pass=null, bool $graphical_output=false, bool $result_output=false, bool $show_question_only=true, bool $show_feedback=false, bool $show_correct_solution=false, bool $show_manual_scoring=false, bool $show_question_text=true, bool $show_inline_feedback=true)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output=false, bool $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output where latex code may be in it If the text is HTML-free...
This class represents a key value pair wizard property in a property form.
renderEditForm(ilPropertyFormGUI $form)
getGenericFeedbackOutput(int $active_id, ?int $pass)