ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
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  $definitions->setInfo($this->lng->txt('latex_edit_info'));
356  $form->addItem($definitions);
357 
358  $terms = new ilMatchingWizardInputGUI($this->lng->txt("terms"), "terms");
359  if ($this->object->getSelfAssessmentEditingMode()) {
360  $terms->setHideImages(true);
361  }
362  $terms->setRequired(true);
363  $terms->setQuestionObject($this->object);
364  $terms->setTextName($this->lng->txt('term_text'));
365  $terms->setImageName($this->lng->txt('term_image'));
366 
367  if (0 === count($this->object->getTerms())) {
368  // @PHP8-CR: If you look above, how $this->object->addDefinition does in fact take an object, I take this
369  // issue as an indicator for a bigger issue and won't suppress / "quickfix" this but postpone further
370  // analysis, eventually involving T&A TechSquad
371  $this->object->addTerm(new assAnswerMatchingTerm());
372  }
373  $termvalues = array_map($stripHtmlEntitesFromValues, $this->object->getTerms());
374  $terms->setValues($termvalues);
375  if ($this->isTermImgUploadCommand()) {
376  $terms->checkInput();
377  }
378  $terms->setInfo($this->lng->txt('latex_edit_info'));
379  $form->addItem($terms);
380 
381  $pairs = new ilMatchingPairWizardInputGUI($this->lng->txt('matching_pairs'), 'pairs');
382  $pairs->setRequired(true);
383  $pairs->setTerms($this->object->getTerms());
384  $pairs->setDefinitions($this->object->getDefinitions());
385  if (count($this->object->getMatchingPairs()) == 0) {
386  $this->object->addMatchingPair($termvalues[0], $definitionvalues[0], 0);
387  //$this->object->addMatchingPair(new assAnswerMatchingPair($termvalues[0], $definitionvalues[0], 0));
388  }
389  $pairs->setPairs($this->object->getMatchingPairs());
390  $form->addItem($pairs);
391 
392  return $form;
393  }
394 
396  {
397  // Edit mode
398  $hidden = new ilHiddenInputGUI("matching_type");
399  $hidden->setValue('');
400  $form->addItem($hidden);
401 
402  if (!$this->object->getSelfAssessmentEditingMode()) {
403  // shuffle
404  $shuffle = new ilSelectInputGUI($this->lng->txt("shuffle_answers"), "shuffle");
405  $shuffle_options = [
406  0 => $this->lng->txt("no"),
407  1 => $this->lng->txt("matching_shuffle_terms_definitions"),
408  2 => $this->lng->txt("matching_shuffle_terms"),
409  3 => $this->lng->txt("matching_shuffle_definitions")
410  ];
411  $shuffle->setOptions($shuffle_options);
412  $shuffle->setValue($this->object->getShuffleMode());
413  $shuffle->setRequired(false);
414  $form->addItem($shuffle);
415 
416  $geometry = new ilNumberInputGUI($this->lng->txt('thumb_size'), 'thumb_geometry');
417  $geometry->setValue((string) $this->object->getThumbGeometry());
418  $geometry->setRequired(true);
419  $geometry->setMaxLength(6);
420  $geometry->setMinValue($this->object->getMinimumThumbSize());
421  $geometry->setMaxValue($this->object->getMaximumThumbSize());
422  $geometry->setSize(6);
423  $geometry->setInfo($this->lng->txt('thumb_size_info'));
424  $form->addItem($geometry);
425  }
426 
427  // Matching Mode
428  $mode = new ilRadioGroupInputGUI($this->lng->txt('qpl_qst_inp_matching_mode'), 'matching_mode');
429  $mode->setRequired(true);
430 
431  $modeONEonONE = new ilRadioOption(
432  $this->lng->txt('qpl_qst_inp_matching_mode_one_on_one'),
434  );
435  $mode->addOption($modeONEonONE);
436 
437  $modeALLonALL = new ilRadioOption(
438  $this->lng->txt('qpl_qst_inp_matching_mode_all_on_all'),
440  );
441  $mode->addOption($modeALLonALL);
442 
443  $mode->setValue($this->object->getMatchingMode());
444 
445  $form->addItem($mode);
446  return $form;
447  }
448 
449  public function getSolutionOutput(
450  int $active_id,
451  ?int $pass = null,
452  bool $graphical_output = false,
453  bool $result_output = false,
454  bool $show_question_only = true,
455  bool $show_feedback = false,
456  bool $show_correct_solution = false,
457  bool $show_manual_scoring = false,
458  bool $show_question_text = true,
459  bool $show_inline_feedback = true
460  ): string {
461  $solutions = [];
462  if (($active_id > 0) && (!$show_correct_solution)) {
463  $solutions = $this->object->getSolutionValues($active_id, $pass);
464  } else {
465  foreach ($this->object->getMaximumScoringMatchingPairs() as $pair) {
466  $solutions[] = [
467  'value1' => $pair->getTerm()->getIdentifier(),
468  'value2' => $pair->getDefinition()->getIdentifier(),
469  'points' => $pair->getPoints()
470  ];
471  }
472  }
473 
474  return $this->renderSolutionOutput(
475  $solutions,
476  $active_id,
477  $pass,
478  $graphical_output,
479  $result_output,
480  $show_question_only,
481  $show_feedback,
482  $show_correct_solution,
483  $show_manual_scoring,
484  $show_question_text,
485  false,
486  $show_inline_feedback,
487  );
488  }
489 
490  public function renderSolutionOutput(
491  mixed $user_solutions,
492  int $active_id,
493  ?int $pass,
494  bool $graphical_output = false,
495  bool $result_output = false,
496  bool $show_question_only = true,
497  bool $show_feedback = false,
498  bool $show_correct_solution = false,
499  bool $show_manual_scoring = false,
500  bool $show_question_text = true,
501  bool $show_autosave_title = false,
502  bool $show_inline_feedback = false,
503  ): ?string {
504  $template = new ilTemplate('tpl.il_as_qpl_matching_output_solution.html', true, true, 'components/ILIAS/TestQuestionPool');
505  $solutiontemplate = new ilTemplate('tpl.il_as_tst_solution_output.html', true, true, 'components/ILIAS/TestQuestionPool');
506  $i = 0;
507 
508  foreach ($user_solutions as $solution) {
509  $definition = $this->object->getDefinitionWithIdentifier($solution['value2']);
510  $term = $this->object->getTermWithIdentifier($solution['value1']);
511  $points = $solution['points'];
512 
513  if (is_object($definition)) {
514  if ($definition->getPicture() !== '') {
515  if ($definition->getText() !== '') {
516  $template->setCurrentBlock('definition_image_text');
517  $template->setVariable(
518  'TEXT_DEFINITION',
519  ilLegacyFormElementsUtil::prepareFormOutput($definition->getText())
520  );
521  $template->parseCurrentBlock();
522  }
523 
524  $answerImageSrc = ilWACSignedPath::signFile(
525  $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $definition->getPicture()
526  );
527 
528  $template->setCurrentBlock('definition_image');
529  $template->setVariable('ANSWER_IMAGE_URL', $answerImageSrc);
530  $template->setVariable(
531  'ANSWER_IMAGE_ALT',
532  (strlen($definition->getText())) ? ilLegacyFormElementsUtil::prepareFormOutput(
533  $definition->getText()
534  ) : ilLegacyFormElementsUtil::prepareFormOutput($definition->getPicture())
535  );
536  $template->setVariable(
537  'ANSWER_IMAGE_TITLE',
538  (strlen($definition->getText())) ? ilLegacyFormElementsUtil::prepareFormOutput(
539  $definition->getText()
540  ) : ilLegacyFormElementsUtil::prepareFormOutput($definition->getPicture())
541  );
542  $template->setVariable('URL_PREVIEW', $this->object->getImagePathWeb() . $definition->getPicture());
543  $template->setVariable("TEXT_PREVIEW", $this->lng->txt('preview'));
544  $template->setVariable("IMG_PREVIEW", ilUtil::getImagePath('media/enlarge.svg'));
545  $template->parseCurrentBlock();
546  } else {
547  $template->setCurrentBlock('definition_text');
548  $template->setVariable("DEFINITION", ilLegacyFormElementsUtil::prepareTextareaOutput($definition->getText(), true));
549  $template->parseCurrentBlock();
550  }
551  }
552  if ($term !== null) {
553  if (strlen($term->getPicture())) {
554  if (strlen($term->getText())) {
555  $template->setCurrentBlock('term_image_text');
556  $template->setVariable("TEXT_TERM", ilLegacyFormElementsUtil::prepareFormOutput($term->getText()));
557  $template->parseCurrentBlock();
558  }
559 
560  $answerImageSrc = ilWACSignedPath::signFile(
561  $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $term->getPicture()
562  );
563 
564  $template->setCurrentBlock('term_image');
565  $template->setVariable('ANSWER_IMAGE_URL', $answerImageSrc);
566  $template->setVariable(
567  'ANSWER_IMAGE_ALT',
568  (strlen($term->getText())) ? ilLegacyFormElementsUtil::prepareFormOutput(
569  $term->getText()
570  ) : ilLegacyFormElementsUtil::prepareFormOutput($term->getPicture())
571  );
572  $template->setVariable(
573  'ANSWER_IMAGE_TITLE',
574  (strlen($term->getText())) ? ilLegacyFormElementsUtil::prepareFormOutput(
575  $term->getText()
576  ) : ilLegacyFormElementsUtil::prepareFormOutput($term->getPicture())
577  );
578  $template->setVariable('URL_PREVIEW', $this->object->getImagePathWeb() . $term->getPicture());
579  $template->setVariable("TEXT_PREVIEW", $this->lng->txt('preview'));
580  $template->setVariable("IMG_PREVIEW", ilUtil::getImagePath('media/enlarge.svg'));
581  $template->parseCurrentBlock();
582  } else {
583  $template->setCurrentBlock('term_text');
584  $template->setVariable("TERM", ilLegacyFormElementsUtil::prepareTextareaOutput($term->getText(), true));
585  $template->parseCurrentBlock();
586  }
587  $i++;
588  }
589  if (($active_id > 0) && (!$show_correct_solution)) {
590  if ($graphical_output) {
591  // output of ok/not ok icons for user entered solutions
592  $ok = false;
593  foreach ($this->object->getMatchingPairs() as $pair) {
594  if ($this->isCorrectMatching($pair, $definition, $term)) {
595  $ok = true;
596  }
597  }
598 
599  $correctness_icon = $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_NOT_OK);
600  if ($ok) {
601  $correctness_icon = $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_OK);
602  }
603  $template->setCurrentBlock('icon_ok');
604  $template->setVariable('ICON_OK', $correctness_icon);
605  $template->parseCurrentBlock();
606  }
607  }
608 
609  if ($result_output) {
610  $resulttext = ($points == 1) ? '(%s ' . $this->lng->txt('point') . ')' : '(%s ' . $this->lng->txt('points') . ')';
611  $template->setCurrentBlock('result_output');
612  $template->setVariable('RESULT_OUTPUT', sprintf($resulttext, $points));
613  $template->parseCurrentBlock();
614  }
615 
616  $template->setCurrentBlock('row');
617  $template->setVariable('TEXT_MATCHES', $this->lng->txt('matches'));
618  $template->parseCurrentBlock();
619  }
620 
621  if ($show_question_text == true) {
622  $template->setVariable('QUESTIONTEXT', $this->object->getQuestionForHTMLOutput());
623  }
624 
625  $questionoutput = $template->get();
626 
627  $feedback = '';
628  if ($show_feedback) {
629  if (!$this->isTestPresentationContext()) {
630  $fb = $this->getGenericFeedbackOutput((int) $active_id, $pass);
631  $feedback .= strlen($fb) ? $fb : '';
632  }
633 
634  $fb = $this->getSpecificFeedbackOutput([]);
635  $feedback .= strlen($fb) ? $fb : '';
636  }
637  if (strlen($feedback)) {
638  $cssClass = (
639  $this->hasCorrectSolution($active_id, $pass) ?
641  );
642 
643  $solutiontemplate->setVariable('ILC_FB_CSS_CLASS', $cssClass);
644  $solutiontemplate->setVariable('FEEDBACK', ilLegacyFormElementsUtil::prepareTextareaOutput($feedback, true));
645  }
646 
647  $solutiontemplate->setVariable('SOLUTION_OUTPUT', $questionoutput);
648 
649  $solutionoutput = $solutiontemplate->get();
650  if (!$show_question_only) {
651  // get page object output
652  $solutionoutput = $this->getILIASPage($solutionoutput);
653  }
654  return $solutionoutput;
655  }
656 
657  public function getPreview(
658  bool $show_question_only = false,
659  bool $show_inline_feedback = false
660  ): string {
661  $template = new ilTemplate('tpl.il_as_qpl_matching_output.html', true, true, 'components/ILIAS/TestQuestionPool');
662  $this->initializePlayerJS();
663 
664  $solutions = $this->getPreviewSession()?->getParticipantsSolution() ?? [];
665 
666  // shuffle output
667  $terms = $this->object->getTerms();
668  $definitions = $this->object->getDefinitions();
669  switch ($this->object->getShuffleMode()) {
670  case 1:
671  $terms = $this->object->getShuffler()->transform($terms);
672  $definitions = $this->object->getShuffler()->transform(
673  $this->object->getShuffler()->transform($definitions)
674  );
675  break;
676  case 2:
677  $terms = $this->object->getShuffler()->transform($terms);
678  break;
679  case 3:
680  $definitions = $this->object->getShuffler()->transform($definitions);
681  break;
682  }
683 
684  foreach ($definitions as $definition) {
685  $terms = $this->populateDefinition($template, $definition, $solutions, $terms);
686  $template->setCurrentBlock('droparea');
687  $template->setVariable('ID_DROPAREA', $definition->getIdentifier());
688  $template->setVariable('QUESTION_ID', $this->object->getId());
689  $template->parseCurrentBlock();
690  }
691 
692  $template->setVariable(
693  'TERMS_PRESENTATION_SOURCE',
694  array_reduce(
695  $terms,
696  fn(string $c, assAnswerMatchingTerm $v) => $c . $this->buildTermHtml($v),
697  ''
698  )
699  );
700 
701  $template->setVariable('QUESTIONTEXT', $this->renderLatex($this->object->getQuestionForHTMLOutput()));
702 
703  $questionoutput = $template->get();
704 
705  if (!$show_question_only) {
706  // get page object output
707  $questionoutput = $this->getILIASPage($questionoutput);
708  }
709 
710  return $questionoutput;
711  }
712 
713  private function populateDefinition(
714  ilTemplate $template,
715  assAnswerMatchingDefinition $definition,
716  array $solutions,
717  array $terms
718  ): array {
719  if ($definition->getPicture() !== '') {
720  $template->setCurrentBlock('definition_picture');
721  $template->setVariable('DEFINITION_ID', $definition->getIdentifier());
722  $template->setVariable('IMAGE_HREF', $this->object->getImagePathWeb() . $definition->getPicture());
723  $thumbweb = $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $definition->getPicture();
724  $thumb = $this->object->getImagePath() . $this->object->getThumbPrefix() . $definition->getPicture();
725  if (!file_exists($thumb)) {
726  $this->object->rebuildThumbnails();
727  }
728  $template->setVariable('THUMBNAIL_HREF', $thumbweb);
729  $template->setVariable('THUMB_ALT', $this->lng->txt('image'));
730  $template->setVariable('THUMB_TITLE', $this->lng->txt('image'));
731  $template->setVariable('TEXT_DEFINITION', (strlen($definition->getText())) ? $this->renderLatex(
732  ilLegacyFormElementsUtil::prepareTextareaOutput($definition->getText(), true, true)
733  ) : '');
734  $template->setVariable('TEXT_PREVIEW', $this->lng->txt('preview'));
735  $template->setVariable('IMG_PREVIEW', ilUtil::getImagePath('media/enlarge.svg'));
736  $template->parseCurrentBlock();
737  } else {
738  $template->setCurrentBlock('definition_text');
739  $template->setVariable('DEFINITION', $this->renderLatex(
740  ilLegacyFormElementsUtil::prepareTextareaOutput($definition->getText(), true, true)
741  ));
742  $template->parseCurrentBlock();
743  }
744 
745  if ($solutions === []
746  || !array_key_exists($definition->getIdentifier(), $solutions)) {
747  $template->setVariable('ASSIGNED_TERMS', json_encode([]));
748  return $terms;
749  }
750 
751  return $this->populateAssignedTerms($template, $definition->getIdentifier(), $solutions[$definition->getIdentifier()], $terms);
752  }
753 
757  private function populateAssignedTerms(
758  ilTemplate $definition_template,
759  int $definition_id,
760  array $assigned_term_ids,
761  array $available_terms
762  ): array {
763  $definition_template->setVariable('ASSIGNED_TERMS', json_encode($assigned_term_ids));
764  $definition_template->setVariable(
765  'TERMS_PRESENTATION_ASSIGNED',
766  array_reduce(
767  $assigned_term_ids,
768  function (string $c, int $v) use ($definition_id, &$available_terms) {
769  $key = $this->getArrayKeyForTermId($v, $available_terms);
770  if ($key === null) {
771  return $c;
772  }
773  $c .= $this->buildTermHtml($available_terms[$key], $definition_id);
774  if ($this->object->getMatchingMode() === assMatchingQuestion::MATCHING_MODE_1_ON_1) {
775  unset($available_terms[$key]);
776  }
777  return $c;
778  },
779  ''
780  )
781  );
782 
783  return $available_terms;
784  }
785 
786  private function getArrayKeyForTermId(int $term_id, array $terms): ?int
787  {
788  foreach ($terms as $key => $term) {
789  if ($term->getIdentifier() === $term_id) {
790  return $key;
791  }
792  }
793  return null;
794  }
795 
796  private function buildTermHtml(assAnswerMatchingTerm $term, ?int $definition_id = null): string
797  {
798  $template = new ilTemplate('tpl.il_as_qpl_matching_term_output.html', true, true, 'components/ILIAS/TestQuestionPool');
799 
800  $template->setVariable('ID_DRAGGABLE', $term->getIdentifier());
801 
802  if ($definition_id !== null) {
803  $template->setCurrentBlock('definition_id');
804  $template->setVariable('IID_DROPAREA', $definition_id);
805  $template->parseCurrentBlock();
806  }
807 
808  if ($term->getPicture() === '') {
809  $template->setCurrentBlock('term_text');
810  $template->setVariable('TERM_TEXT', $this->renderLatex(
812  ));
813  $template->parseCurrentBlock();
814  return $template->get();
815  }
816 
817  $template->setCurrentBlock('term_picture');
818  $template->setVariable('TERM_ID', $term->getIdentifier());
819  $template->setVariable('IMAGE_HREF', $this->object->getImagePathWeb() . $term->getPicture());
820  $thumbweb = $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $term->getPicture();
821  $thumb = $this->object->getImagePath() . $this->object->getThumbPrefix() . $term->getPicture();
822  if (!file_exists($thumb)) {
823  $this->object->rebuildThumbnails();
824  }
825  $template->setVariable('THUMBNAIL_HREF', $thumbweb);
826  $template->setVariable('THUMB_ALT', $this->lng->txt('image'));
827  $template->setVariable('THUMB_TITLE', $this->lng->txt('image'));
828  $template->setVariable('TEXT_PREVIEW', $this->lng->txt('preview'));
829  $template->setVariable('TEXT_TERM', $term->getText() !== ''
830  ? $this->renderLatex(
832  )
833  : '');
834  $template->setVariable('IMG_PREVIEW', ilUtil::getImagePath('media/enlarge.svg'));
835  $template->parseCurrentBlock();
836  return $template->get();
837  }
838 
839  private function buildSolutionsArray(int $active_id, int $attempt, array|bool $user_post_solutions): array
840  {
841  if ($active_id === 0) {
842  return [];
843  }
844  if ($user_post_solutions !== false) {
845  return $user_post_solutions['matching'];
846  }
847 
848  return array_reduce(
849  $this->object->getTestOutputSolutions($active_id, $attempt),
850  static function (array $c, array $v): array {
851  if (!array_key_exists($v['value2'], $c)) {
852  $c[$v['value2']] = [$v['value1']];
853  return $c;
854  }
855  $c[$v['value2']][] = $v['value1'];
856  return $c;
857  },
858  []
859  );
860  }
861 
867  protected function sortDefinitionsBySolution(array $solutions, array $definitions): array
868  {
869  $neworder = [];
870  $handled_definitions = [];
871  foreach (array_keys($solutions) as $definition_id) {
872  $neworder[] = $this->object->getDefinitionWithIdentifier($definition_id);
873  $handled_definitions[$definition_id] = $definition_id;
874  }
875 
876  foreach ($definitions as $definition) {
877  if (!isset($handled_definitions[$definition->getIdentifier()])) {
878  $neworder[] = $definition;
879  }
880  }
881 
882  return $neworder;
883  }
884 
885  public function getTestOutput(
886  int $active_id,
887  int $attempt,
888  bool $is_question_postponed = false,
889  array|bool $user_post_solutions = false,
890  bool $show_specific_inline_feedback = false
891  ): string {
892  $template = new ilTemplate('tpl.il_as_qpl_matching_output.html', true, true, 'components/ILIAS/TestQuestionPool');
893  $this->initializePlayerJS();
894 
895  $solutions = $this->buildSolutionsArray($active_id, $attempt, $user_post_solutions);
896  $terms = $this->object->getTerms();
897  $definitions = $this->object->getDefinitions();
898  switch ($this->object->getShuffleMode()) {
899  case 1:
900  $terms = $this->object->getShuffler()->transform($terms);
901  if ($solutions !== []) {
902  $definitions = $this->sortDefinitionsBySolution($solutions, $definitions);
903  } else {
904  $definitions = $this->object->getShuffler()->transform(
905  $this->object->getShuffler()->transform($definitions)
906  );
907  }
908  break;
909  case 2:
910  $terms = $this->object->getShuffler()->transform($terms);
911  break;
912  case 3:
913  if ($solutions !== []) {
914  $definitions = $this->sortDefinitionsBySolution($solutions, $definitions);
915  } else {
916  $definitions = $this->object->getShuffler()->transform($definitions);
917  }
918  break;
919  }
920 
921  foreach ($definitions as $definition) {
922  $terms = $this->populateDefinition($template, $definition, $solutions, $terms);
923  $template->setCurrentBlock('droparea');
924  $template->setVariable('ID_DROPAREA', $definition->getIdentifier());
925  $template->setVariable('QUESTION_ID', $this->object->getId());
926  $template->parseCurrentBlock();
927  }
928 
929  $template->setVariable(
930  'TERMS_PRESENTATION_SOURCE',
931  array_reduce(
932  $terms,
933  fn(string $c, assAnswerMatchingTerm $v) => $c . $this->buildTermHtml($v),
934  ''
935  )
936  );
937 
938  $template->setVariable('QUESTIONTEXT', $this->renderLatex($this->object->getQuestionForHTMLOutput()));
939 
940  return $this->outQuestionPage('', $is_question_postponed, $active_id, $template->get());
941  }
942 
946  public function checkInput(): bool
947  {
948  $title = $this->request_data_collector->string('title');
949  $author = $this->request_data_collector->string('author');
950  $question = $this->request_data_collector->string('question');
951 
952  return !empty($title) && !empty($author) && !empty($question);
953  }
954 
955  public function getSpecificFeedbackOutput(array $userSolution): string
956  {
957  $matches = array_values($this->object->matchingpairs);
958 
959  if (!$this->object->feedbackOBJ->specificAnswerFeedbackExists()) {
960  return '';
961  }
962 
963  $feedback = '<table class="test_specific_feedback"><tbody>';
964 
965  foreach ($matches as $idx => $ans) {
966  if (!isset($userSolution[$ans->getDefinition()->getIdentifier()])) {
967  continue;
968  }
969 
970  if (!is_array($userSolution[$ans->getDefinition()->getIdentifier()])) {
971  continue;
972  }
973 
974  if (!in_array($ans->getTerm()->getIdentifier(), $userSolution[$ans->getDefinition()->getIdentifier()])) {
975  continue;
976  }
977 
978  $fb = $this->object->feedbackOBJ->getSpecificAnswerFeedbackTestPresentation(
979  $this->object->getId(),
980  0,
981  $idx
982  );
983  $feedback .= "<tr><td>\"{$ans->getDefinition()->getText()}\" {$this->lng->txt('matches')} ";
984  $feedback .= "\"{$ans->getTerm()->getText()}\"</td><td>{$fb}</td></tr>";
985  }
986 
987  $feedback .= '</tbody></table>';
988  return $this->renderLatex(ilLegacyFormElementsUtil::prepareTextareaOutput($feedback, true));
989  }
990 
1001  {
1002  return [];
1003  }
1004 
1015  {
1016  return [];
1017  }
1018 
1019  private function isCorrectMatching($pair, $definition, $term): bool
1020  {
1021  if (!($pair->getPoints() > 0)) {
1022  return false;
1023  }
1024 
1025  if (!is_object($term)) {
1026  return false;
1027  }
1028 
1029  if ($pair->getDefinition()->getIdentifier() != $definition->getIdentifier()) {
1030  return false;
1031  }
1032 
1033  if ($pair->getTerm()->getIdentifier() != $term->getIdentifier()) {
1034  return false;
1035  }
1036 
1037  return true;
1038  }
1039 
1040  protected function getAnswerStatisticImageHtml($picture): string
1041  {
1042  $thumbweb = $this->object->getImagePathWeb() . $this->object->getThumbPrefix() . $picture;
1043  return '<img src="' . $thumbweb . '" alt="' . $picture . '" title="' . $picture . '"/>';
1044  }
1045 
1046  protected function getAnswerStatisticMatchingElemHtml($elem): string
1047  {
1048  $html = '';
1049 
1050  if (strlen($elem->getText())) {
1051  $html .= $elem->getText();
1052  }
1053 
1054  if (strlen($elem->getPicture())) {
1055  $html .= $this->getAnswerStatisticImageHtml($elem->getPicture());
1056  }
1057 
1058  return $html;
1059  }
1060 
1061  public function getAnswersFrequency($relevantAnswers, $questionIndex): array
1062  {
1063  $answersByActiveAndPass = [];
1064 
1065  foreach ($relevantAnswers as $row) {
1066  $key = $row['active_fi'] . ':' . $row['pass'];
1067 
1068  if (!isset($answersByActiveAndPass[$key])) {
1069  $answersByActiveAndPass[$key] = [];
1070  }
1071 
1072  $answersByActiveAndPass[$key][$row['value1']] = $row['value2'];
1073  }
1074 
1075  $answers = [];
1076 
1077  foreach ($answersByActiveAndPass as $key => $matchingPairs) {
1078  foreach ($matchingPairs as $termId => $defId) {
1079  $hash = md5($termId . ':' . $defId);
1080 
1081  if (!isset($answers[$hash])) {
1082  $termHtml = $this->getAnswerStatisticMatchingElemHtml(
1083  $this->object->getTermWithIdentifier($termId)
1084  );
1085 
1086  $defHtml = $this->getAnswerStatisticMatchingElemHtml(
1087  $this->object->getDefinitionWithIdentifier($defId)
1088  );
1089 
1090  $answers[$hash] = [
1091  'answer' => $termHtml . $defHtml,
1092  'term' => $termHtml,
1093  'definition' => $defHtml,
1094  'frequency' => 0
1095  ];
1096  }
1097 
1098  $answers[$hash]['frequency']++;
1099  }
1100  }
1101 
1102  return $answers;
1103  }
1104 
1112  public function getAnswerFrequencyTableGUI($parentGui, $parentCmd, $relevantAnswers, $questionIndex): ilAnswerFrequencyStatisticTableGUI
1113  {
1114  $table = new ilMatchingQuestionAnswerFreqStatTableGUI($parentGui, $parentCmd, $this->object);
1115  $table->setQuestionIndex($questionIndex);
1116  $table->setData($this->getAnswersFrequency($relevantAnswers, $questionIndex));
1117  $table->initColumns();
1118 
1119  return $table;
1120  }
1121 
1123  {
1124  $pairs = new ilAssMatchingPairCorrectionsInputGUI($this->lng->txt('matching_pairs'), 'pairs');
1125  $pairs->setRequired(true);
1126  $pairs->setTerms($this->object->getTerms());
1127  $pairs->setDefinitions($this->object->getDefinitions());
1128  $pairs->setPairs($this->object->getMatchingPairs());
1129  $pairs->setThumbsWebPathWithPrefix($this->object->getImagePathWeb() . $this->object->getThumbPrefix());
1130  $form->addItem($pairs);
1131  }
1132 
1137  {
1138  $pairs = $this->object->getMatchingPairs();
1139  $nu_pairs = [];
1140 
1141  if ($this->request_data_collector->isset('pairs')) {
1142  $points_of_pairs = $this->request_data_collector->raw('pairs')['points'];
1143  $pair_terms = explode(',', $this->request_data_collector->raw('pairs')['term_id']);
1144  $pair_definitions = explode(',', $this->request_data_collector->raw('pairs')['definition_id']);
1145  $values = [];
1146  foreach ($points_of_pairs as $idx => $points) {
1147  $k = implode('.', [$pair_terms[$idx], $pair_definitions[$idx]]);
1148  $values[$k] = (float) str_replace(',', '.', $points);
1149  }
1150 
1151  foreach ($pairs as $idx => $pair) {
1152  $id = implode('.', [
1153  $pair->getTerm()->getIdentifier(),
1154  $pair->getDefinition()->getIdentifier()
1155  ]);
1156  $nu_pairs[$id] = $pair->withPoints($values[$id]);
1157  }
1158 
1159  $this->object = $this->object->withMatchingPairs($nu_pairs);
1160  }
1161  }
1162 
1163  private function initializePlayerJS(): void
1164  {
1165  $this->tpl->addJavaScript('assets/js/matching.js');
1166  $this->tpl->addOnLoadCode(
1167  'il.test.matching.init('
1168  . "document.querySelector('#ilMatchingQuestionContainer_{$this->object->getId()}'),"
1169  . "'{$this->object->getMatchingMode()}');"
1170  );
1171  }
1172 }
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...
renderLatex($content)
Wrap content with latex in a LatexContent UI component and render it to be processed by MathJax in th...
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)