ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.assMatchingQuestionGUI.php
Go to the documentation of this file.
1<?php
2
19declare(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) {
60 return 0;
61 }
62 return 1;
63 }
64
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);
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
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(
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}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
$filename
Definition: buildRTE.php:78
setVariable($variable, $value='')
Sets a variable value.
Definition: IT.php:544
setVariable(string $a_group_name, string $a_var_name, string $a_var_value)
sets a variable in a group
return true
Class for matching question definitions.
Class for matching question terms.
Matching question GUI representation.
isValidTermAndDefinitionAmount(ilPropertyFormGUI $form)
for mode 1:1 terms count must not be less than definitions count for mode n:n this limitation is canc...
getPreview(bool $show_question_only=false, bool $show_inline_feedback=false)
populateAnswerSpecificFormPart(\ilPropertyFormGUI $form)
saveCorrectionsFormProperties(ilPropertyFormGUI $form)
populateAssignedTerms(ilTemplate $definition_template, int $definition_id, array $assigned_term_ids, array $available_terms)
getAnswersFrequency($relevantAnswers, $questionIndex)
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,)
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)
populateDefinition(ilTemplate $template, assAnswerMatchingDefinition $definition, array $solutions, array $terms)
editQuestion(bool $checkonly=false, ?bool $is_save_cmd=null)
isCorrectMatching($pair, $definition, $term)
getAfterParticipationSuppressionAnswerPostVars()
Returns a list of postvars which will be suppressed in the form output when used in scoring adjustmen...
getArrayKeyForTermId(int $term_id, array $terms)
getAfterParticipationSuppressionQuestionPostVars()
Returns a list of postvars which will be suppressed in the form output when used in scoring adjustmen...
sortDefinitionsBySolution(array $solutions, array $definitions)
writeQuestionSpecificPostData(ilPropertyFormGUI $form)
Extracts the question specific values from the request and applies them to the data object.
getTestOutput(int $active_id, int $attempt, bool $is_question_postponed=false, array|bool $user_post_solutions=false, bool $show_specific_inline_feedback=false)
buildSolutionsArray(int $active_id, int $attempt, array|bool $user_post_solutions)
getSpecificFeedbackOutput(array $userSolution)
Returns the answer specific feedback for the question.
writeAnswerSpecificPostData(ilPropertyFormGUI $form)
Extracts the answer specific values from the request and applies them to the data object.
populateCorrectionsFormProperties(ilPropertyFormGUI $form)
buildTermHtml(assAnswerMatchingTerm $term, ?int $definition_id=null)
writePostData(bool $always=false)
{Evaluates a posted edit form and writes the form data in the question object.integer A positive valu...
getAnswerFrequencyTableGUI($parentGui, $parentCmd, $relevantAnswers, $questionIndex)
populateQuestionSpecificFormPart(\ilPropertyFormGUI $form)
Class for matching questions.
populateTaxonomyFormSection(ilPropertyFormGUI $form)
addBasicQuestionFormProperties(ilPropertyFormGUI $form)
renderEditForm(ilPropertyFormGUI $form)
addQuestionFormCommandButtons(ilPropertyFormGUI $form)
setErrorMessage(string $errormessage)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This class represents a hidden form property in a property form.
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,...
static prepareFormOutput($a_str, bool $a_strip=false)
This class represents a key value pair wizard property in a property form.
This class represents a single choice wizard property in a property form.
This class represents a number property in a property form.
This class represents a property form user interface.
getItemByPostVar(string $a_post_var)
This class represents a property in a property form.
This class represents an option in a radio group.
This class represents a selection list property in a property form.
special template class to simplify handling of ITX/PEAR
setCurrentBlock(string $part=ilGlobalTemplateInterface::DEFAULT_BLOCK)
parseCurrentBlock(string $part=ilGlobalTemplateInterface::DEFAULT_BLOCK)
static getImagePath(string $image_name, string $module_path="", string $mode="output", bool $offline=false)
get image path (for images located in a template directory)
static stripSlashes(string $a_str, bool $a_strip_html=true, string $a_allow="")
static signFile(string $path_to_file)
$c
Definition: deliver.php:25
A transformation is a function from one datatype to another.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
if(!file_exists('../ilias.ini.php'))