ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
class.assClozeTestGUI.php
Go to the documentation of this file.
1 <?php
2 
21 use ILIAS\Refinery\Random\Group as RandomGroup;
23 
40 {
41  public const JS_INSERT_GAP_CODE_AT_CARET = <<<JS
42  jQuery.fn.extend({
43  insertGapCodeAtCaret: function() {
44  return this.each(function(i) {
45  var code_start = '[gap]';
46  var code_end = '[/gap]';
47  if (typeof tinyMCE !== 'undefined' && typeof tinyMCE.get('cloze_text') !== 'undefined') {
48  var ed = tinyMCE.get('cloze_text');
49  il.ClozeHelper.internetExplorerTinyMCECursorFix(ed);
50  ed.selection.setContent(code_start + ed.selection.getContent() + code_end);
51  ed.focus();
52  return;
53  }
54  if (document.selection) {
55  //For browsers like Internet Explorer
56  this.focus();
57  sel = document.selection.createRange();
58  sel.text = code_start + sel.text + code_end;
59  this.focus();
60  } else if (this.selectionStart || this.selectionStart == '0') {
61  //For browsers like Firefox and Webkit based
62  var startPos = this.selectionStart;
63  var endPos = this.selectionEnd;
64  var scrollTop = this.scrollTop;
65  this.value = this.value.substring(0, startPos)
66  + code_start
67  + this.value.substring(startPos, endPos)
68  + code_end
69  + this.value.substring(endPos, this.value.length);
70  this.focus();
71  this.scrollTop = scrollTop;
72  } else {
73  this.value += code_start + code_end;
74  this.focus();
75  }
76  });
77  }
78  });
80 
84  private $gapIndex;
85 
86  private RandomGroup $randomGroup;
89  private ilDBInterface $db;
90 
96  public function __construct(int $id = -1)
97  {
99  global $DIC;
100  $this->ui_factory = $DIC['ui.factory'];
101  $this->ui_renderer = $DIC['ui.renderer'];
102  $this->db = $DIC['ilDB'];
103 
104  $this->object = new assClozeTest();
105  if ($id >= 0) {
106  $this->object->loadFromDb($id);
107  }
108 
109  $this->randomGroup = $this->refinery->random();
110  }
111 
112  public function getCommand($cmd)
113  {
114  if (preg_match("/^(removegap|addgap)_(\d+)$/", $cmd, $matches)) {
115  $cmd = $matches[1];
116  $this->gapIndex = $matches[2];
117  }
118  return $cmd;
119  }
120 
124  protected function writePostData(bool $always = false): int
125  {
126  $hasErrors = (!$always) ? $this->editQuestion(true) : false;
127  if ($hasErrors) {
128  return 1;
129  }
130 
131  $cloze_text = $this->removeIndizesFromGapText(
132  $this->request_data_collector->raw('cloze_text')
133  );
134 
135  $this->object->setQuestion(
136  $this->request_data_collector->raw('question')
137  );
138 
140  $this->object->setClozeText($cloze_text);
141  $this->object->setTextgapRating($this->request_data_collector->raw('textgap_rating'));
142  $this->object->setIdenticalScoring($this->request_data_collector->bool('identical_scoring') ?? false);
143  $this->object->setFixedTextLength($this->request_data_collector->int('fixedTextLength') ?? 0);
145  $this->saveTaxonomyAssignments();
146  return 0;
147  }
148 
149  public function writeAnswerSpecificPostData(ilPropertyFormGUI $form): void
150  {
151  $gaps = $this->request_data_collector->strArray('gap');
152  if (empty($gaps)) {
153  return;
154  }
155 
156  if ($this->ctrl->getCmd() !== 'createGaps') {
157  $this->object->clearGapAnswers();
158  }
159 
160  $answer_trafo = $this->refinery->kindlyTo()->listOf($this->refinery->kindlyTo()->string());
161  $points_trafo = $this->refinery->kindlyTo()->listOf($this->refinery->kindlyTo()->float());
162 
163  foreach (array_keys($gaps) as $idx) {
164  $cloze_type = $this->request_data_collector->int('clozetype_' . $idx);
165  $this->object->setGapType($idx, $cloze_type);
166 
167  $gap_idx = $this->request_data_collector->raw('gap_' . $idx);
168 
169  if ($cloze_type === assClozeGap::TYPE_TEXT || $cloze_type === assClozeGap::TYPE_SELECT) {
170  $answer = $answer_trafo->transform($gap_idx['answer'] ?? []);
171  if ($this->ctrl->getCmd() !== 'createGaps') {
172  if (!empty($answer)) {
173  foreach ($answer as $order => $value) {
174  $this->object->addGapAnswer($idx, $order, $value);
175  }
176  } else {
177  $this->object->addGapAnswer($idx, 0, '');
178  }
179  }
180 
181  $points = $points_trafo->transform($gap_idx['points'] ?? []);
182  foreach ($points as $order => $value) {
183  $this->object->setGapAnswerPoints($idx, $order, $value);
184  }
185  }
186 
187  if ($cloze_type === assClozeGap::TYPE_TEXT || $cloze_type === assClozeGap::TYPE_NUMERIC) {
188  $this->object->setGapShuffle(0);
189 
190  $gap_size = $this->request_data_collector->int('gap_' . $idx . '_gapsize');
191  if ($gap_size !== 0) {
192  $this->object->setGapSize($idx, $gap_size);
193  }
194  } else {
195  $this->object->setGapShuffle($idx, $this->request_data_collector->int('shuffle_' . $idx));
196  }
197 
198  if ($cloze_type === assClozeGap::TYPE_NUMERIC && $this->object->getGap($idx) !== null) {
199  $this->object->getGap($idx)->clearItems();
200 
201  $gap_idx_numeric = $this->request_data_collector->float('gap_' . $idx . '_numeric');
202  if ($gap_idx_numeric !== 0.0) {
203  $this->object->addGapAnswer($idx, 0, (string) $gap_idx_numeric);
204  $this->object->setGapAnswerLowerBound(
205  $idx,
206  0,
207  (string) $this->request_data_collector->float('gap_' . $idx . '_numeric_lower')
208  );
209  $this->object->setGapAnswerUpperBound(
210  $idx,
211  0,
212  (string) $this->request_data_collector->float('gap_' . $idx . '_numeric_upper')
213  );
214  $this->object->setGapAnswerPoints(
215  $idx,
216  0,
217  $this->request_data_collector->float('gap_' . $idx . '_numeric_points')
218  );
219  } else {
220  if ($this->ctrl->getCmd() !== 'createGaps') {
221  $this->object->addGapAnswer($idx, 0, '');
222  }
223 
224  $this->object->setGapAnswerLowerBound($idx, 0, '');
225  $this->object->setGapAnswerUpperBound($idx, 0, '');
226  }
227  }
228 
229  $ass_cloze_gab_combination = new assClozeGapCombination($this->db);
230  $ass_cloze_gab_combination->clearGapCombinationsFromDb($this->object->getId());
231 
232  $gap_combination = $this->request_data_collector->rawArray('gap_combination', 4);
233  if ($gap_combination !== []) {
234  $ass_cloze_gab_combination->saveGapCombinationToDb(
235  $this->object->getId(),
236  $gap_combination,
237  $this->request_data_collector->rawArray('gap_combination_values')
238  );
239  $this->object->setGapCombinationsExists(true);
240  }
241  }
242 
243  if ($this->ctrl->getCmd() !== 'createGaps') {
244  $this->object->updateClozeTextFromGaps();
245  }
246  }
247 
249  {
250  $this->object->setClozeText($this->request_data_collector->string('cloze_quest'));
251  $this->object->setTextgapRating($this->request_data_collector->string('textgap_rating'));
252  $this->object->setIdenticalScoring($this->request_data_collector->bool('textgap_rating') ?? true);
253  $this->object->setFixedTextLength($this->request_data_collector->int('fixedTextLength'));
254  }
255 
261  public function editQuestion(
262  bool $checkonly = false,
263  ?bool $is_save_cmd = null
264  ): bool {
265  $save = $is_save_cmd ?? $this->isSaveCommand();
266 
267  $form = new ilPropertyFormGUI();
268  $this->editForm = $form;
269 
270  $form->setFormAction($this->ctrl->getFormAction($this));
271  $form->setTitle($this->outQuestionType());
272  $form->setMultipart(false);
273  $form->setTableWidth("100%");
274  $form->setId("assclozetest");
275 
276  $this->addBasicQuestionFormProperties($form);
277  $this->populateQuestionSpecificFormPart($form);
278  $this->populateAnswerSpecificFormPart($form);
279  $this->populateTaxonomyFormSection($form);
280 
281  $this->addQuestionFormCommandButtons($form);
282 
283  $errors = false;
284 
285  if ($save) {
286  $form->setValuesByPost();
287  $errors = !$form->checkInput();
288  $form->setValuesByPost();
289 
290  $gap_combinations = $this->request_data_collector->raw('gap_combination');
291  if (is_array($gap_combinations)
292  && $gap_combinations !== []
293  && $this->hasErrorInGapCombinationPoints($gap_combinations)) {
294  $this->tpl->setOnScreenMessage('failure', $this->lng->txt('points_non_numeric_or_negative_msg'));
295  $errors = true;
296  }
297 
298  if ($errors) {
299  $checkonly = false;
300  }
301  }
302 
303  if (!$checkonly) {
304  $this->renderEditForm($form);
305  }
306  return $errors;
307  }
308 
309  private function hasErrorInGapCombinationPoints(array $gap_combinations): bool
310  {
311  foreach ($gap_combinations['points'] as $gaps_points) {
312  foreach ($gaps_points as $points) {
313  $points_standardized = str_replace(',', '.', $points);
314  if (!is_numeric($points_standardized)
315  || (float) $points_standardized < 0) {
316  return true;
317  }
318  }
319  }
320 
321  return false;
322  }
323 
325  {
326  // cloze text
327  $cloze_text = new ilTextAreaInputGUI($this->lng->txt("cloze_text"), 'cloze_text');
328  $cloze_text->setRequired(true);
329  $cloze_text->setValue($this->applyIndizesToGapText($this->object->getClozeText()));
330  $cloze_text->setInfo($this->lng->txt("close_text_hint") . ' '
331  . $this->lng->txt('latex_edit_info'));
332  $cloze_text->setRows(10);
333  $cloze_text->setCols(80);
334  if (!$this->object->getSelfAssessmentEditingMode()) {
335  if ($this->object->getAdditionalContentEditingMode() !== assQuestion::ADDITIONAL_CONTENT_EDITING_MODE_IPE) {
336  $cloze_text->setUseRte(true);
337  $cloze_text->setRteTags(ilRTESettings::_getUsedHTMLTags("assessment"));
338  }
339  } else {
341  $cloze_text->setUseTagsForRteOnly(false);
342  }
343  $cloze_text->setRTESupport($this->object->getId(), "qpl", "assessment");
344  $form->addItem($cloze_text);
345 
346  $tpl = new ilTemplate("tpl.il_as_qpl_cloze_gap_button_code.html", true, true, "components/ILIAS/TestQuestionPool");
347 
348  $button = new ilCustomInputGUI('&nbsp;', '');
349 
350  $button_text_gap = $this->ui_factory->button()->standard($this->lng->txt('text_gap'), '')
352  $this->getAddGapButtonClickClosure('text')
353  );
354  $button_select_gap = $this->ui_factory->button()->standard($this->lng->txt('select_gap'), '')
356  $this->getAddGapButtonClickClosure('select')
357  );
358  $button_numeric_gap = $this->ui_factory->button()->standard($this->lng->txt('numeric_gap'), '')
360  $this->getAddGapButtonClickClosure('numeric')
361  );
362 
363  $tpl->setVariable('BUTTON', $this->ui_renderer->render([
364  $button_text_gap,
365  $button_select_gap,
366  $button_numeric_gap
367  ]));
369 
370  $button->setHtml($tpl->get());
371  $this->tpl->addOnloadCode(self::JS_INSERT_GAP_CODE_AT_CARET);
372  $form->addItem($button);
373 
374  // text rating
375  if (!$this->object->getSelfAssessmentEditingMode()) {
376  $textrating = new ilSelectInputGUI($this->lng->txt("text_rating"), "textgap_rating");
377  $text_options = [
378  "ci" => $this->lng->txt("cloze_textgap_case_insensitive"),
379  "cs" => $this->lng->txt("cloze_textgap_case_sensitive"),
380  "l1" => sprintf($this->lng->txt("cloze_textgap_levenshtein_of"), "1"),
381  "l2" => sprintf($this->lng->txt("cloze_textgap_levenshtein_of"), "2"),
382  "l3" => sprintf($this->lng->txt("cloze_textgap_levenshtein_of"), "3"),
383  "l4" => sprintf($this->lng->txt("cloze_textgap_levenshtein_of"), "4"),
384  "l5" => sprintf($this->lng->txt("cloze_textgap_levenshtein_of"), "5")
385  ];
386  $textrating->setOptions($text_options);
387  $textrating->setValue($this->object->getTextgapRating());
388  $form->addItem($textrating);
389 
390  // text field length
391  $fixedTextLength = new ilNumberInputGUI($this->lng->txt("cloze_fixed_textlength"), "fixedTextLength");
392  $ftl = $this->object->getFixedTextLength();
393 
394  $fixedTextLength->setValue($ftl > 0 ? $ftl : '');
395  $fixedTextLength->setMinValue(0);
396  $fixedTextLength->setSize(3);
397  $fixedTextLength->setMaxLength(6);
398  $fixedTextLength->setInfo($this->lng->txt('cloze_fixed_textlength_description'));
399  $fixedTextLength->setRequired(false);
400  $form->addItem($fixedTextLength);
401 
402  // identical scoring
403  $identical_scoring = new ilCheckboxInputGUI($this->lng->txt("identical_scoring"), "identical_scoring");
404  $identical_scoring->setValue(1);
405  $identical_scoring->setChecked($this->object->getIdenticalScoring());
406  $identical_scoring->setInfo($this->lng->txt('identical_scoring_desc'));
407  $identical_scoring->setRequired(false);
408  $form->addItem($identical_scoring);
409  }
410  return $form;
411  }
412 
413  private function getAddGapButtonClickClosure(string $gap_type): Closure
414  {
415  return fn($id) => "var el = document.getElementById('{$id}').addEventListener('click', "
416  . '(e) => {'
417  . ' e.preventDefault();'
418  . " ClozeQuestionGapBuilder.addGapClickFunction('{$gap_type}');"
419  . '});';
420  }
421 
423  {
424  $json = $this->populateJSON();
425  $assClozeGapCombinationObject = new assClozeGapCombination($this->db);
426  $combination_exists = $assClozeGapCombinationObject->combinationExistsForQid($this->object->getId());
427  $combinations = [];
428  if ($combination_exists) {
429  $combinations = $assClozeGapCombinationObject->loadFromDb($this->object->getId());
430  }
431  $new_builder = new ilClozeGapInputBuilderGUI();
432  $header = new ilFormSectionHeaderGUI();
433  $form->addItem($header);
434  $new_builder->setValueByArray($json);
435  $new_builder->setValueCombinationFromDb($combinations);
436  $form->addItem($new_builder);
437  return $form;
438  }
439 
440  protected function populateJSON(): array
441  {
442  $gap = $this->object->getGaps();
443  $array = [];
444  if ($gap == null) {
445  return $array;
446  }
447  $translate_type = ['text','select','numeric'];
448  $i = 0;
449  foreach ($gap as $content) {
450  $shuffle = false;
451  $value = $content->getItemsRaw();
452  $items = [];
453  for ($j = 0, $jMax = count($value); $j < $jMax; $j++) {
454  if ($content->isNumericGap()) {
455  $items[$j] = [
456  'answer' => $value[$j]->getAnswerText(),
457  'lower' => $value[$j]->getLowerBound(),
458  'upper' => $value[$j]->getUpperBound(),
459  'points' => $value[$j]->getPoints(),
460  'error' => false
461  ];
462  } else {
463  $items[$j] = [
464  'answer' => $this->escapeTemplatePlaceholders($value[$j]->getAnswerText()),
465  'points' => $value[$j]->getPoints(),
466  'error' => false
467  ];
468 
469  if ($content->isSelectGap()) {
470  $shuffle = $content->getShuffle();
471  }
472  }
473  }
474  $answers[$i] = [
475  'type' => $translate_type[$content->getType()] ,
476  'values' => $items ,
477  'shuffle' => $shuffle,
478  'text_field_length' => $content->getGapSize() > 0 ? $content->getGapSize() : '',
479  'used_in_gap_combination' => true];
480  $i++;
481  }
482  return $answers;
483  }
495  protected function populateGapFormPart($form, $gapCounter): ilPropertyFormGUI
496  {
497  $gap = $this->object->getGap($gapCounter);
498 
499  if ($gap == null) {
500  return $form;
501  }
502 
503  $header = new ilFormSectionHeaderGUI();
504  $header->setTitle($this->lng->txt("gap") . " " . ($gapCounter + 1));
505  $form->addItem($header);
506 
507  $gapcounter = new ilHiddenInputGUI("gap[$gapCounter]");
508  $gapcounter->setValue($gapCounter);
509  $form->addItem($gapcounter);
510 
511  $gaptype = new ilSelectInputGUI($this->lng->txt('type'), "clozetype_$gapCounter");
512  $options = [
513  0 => $this->lng->txt("text_gap"),
514  1 => $this->lng->txt("select_gap"),
515  2 => $this->lng->txt("numeric_gap")
516  ];
517  $gaptype->setOptions($options);
518  $gaptype->setValue($gap->getType());
519  $form->addItem($gaptype);
520 
521  if ($gap->getType() == assClozeGap::TYPE_TEXT) {
522  $this->populateGapSizeFormPart($form, $gap, $gapCounter);
523 
524  if (count($gap->getItemsRaw()) == 0) {
525  $gap->addItem(new assAnswerCloze("", 0, 0));
526  }
527  $this->populateTextGapFormPart($form, $gap, $gapCounter);
528  } elseif ($gap->getType() == assClozeGap::TYPE_SELECT) {
529  if (count($gap->getItemsRaw()) == 0) {
530  $gap->addItem(new assAnswerCloze("", 0, 0));
531  }
532  $this->populateSelectGapFormPart($form, $gap, $gapCounter);
533  } elseif ($gap->getType() == assClozeGap::TYPE_NUMERIC) {
534  $this->populateGapSizeFormPart($form, $gap, $gapCounter);
535 
536  if (count($gap->getItemsRaw()) == 0) {
537  $gap->addItem(new assAnswerCloze("", 0, 0));
538  }
539  foreach ($gap->getItemsRaw() as $item) {
540  $this->populateNumericGapFormPart($form, $item, $gapCounter);
541  }
542  }
543  return $form;
544  }
545 
551  protected function populateGapSizeFormPart($form, $gap, $gapCounter): ilPropertyFormGUI
552  {
553  $gapSizeFormItem = new ilNumberInputGUI($this->lng->txt('cloze_fixed_textlength'), "gap_" . $gapCounter . '_gapsize');
554 
555  $gapSizeFormItem->allowDecimals(false);
556  $gapSizeFormItem->setMinValue(0);
557  $gapSizeFormItem->setSize(3);
558  $gapSizeFormItem->setMaxLength(6);
559  $gapSizeFormItem->setInfo($this->lng->txt('cloze_gap_size_info'));
560  $gapSizeFormItem->setValue($gap->getGapSize());
561  $form->addItem($gapSizeFormItem);
562 
563  return $form;
564  }
565 
578  protected function populateSelectGapFormPart($form, $gap, $gapCounter): ilPropertyFormGUI
579  {
580  $values = new ilAnswerWizardInputGUI($this->lng->txt("values"), "gap_" . $gapCounter . "");
581  $values->setRequired(true);
582  $values->setQuestionObject($this->object);
583  $values->setSingleline(true);
584  $values->setAllowMove(false);
585 
586  $values->setValues($gap->getItemsRaw());
587  $form->addItem($values);
588 
589  // shuffle
590  $shuffle = new ilCheckboxInputGUI($this->lng->txt("shuffle_answers"), "shuffle_" . $gapCounter . "");
591  $shuffle->setValue(1);
592  $shuffle->setChecked($gap->getShuffle());
593  $shuffle->setRequired(false);
594  $form->addItem($shuffle);
595  return $form;
596  }
597 
609  protected function populateTextGapFormPart($form, $gap, $gapCounter): ilPropertyFormGUI
610  {
611  $values = new ilAnswerWizardInputGUI($this->lng->txt("values"), "gap_" . $gapCounter . "");
612  $values->setRequired(true);
613  $values->setQuestionObject($this->object);
614  $values->setSingleline(true);
615  $values->setAllowMove(false);
616  $values->setValues($gap->getItemsRaw());
617  $form->addItem($values);
618 
619  if ($this->object->getFixedTextLength() > 0) {
620  $values->setSize($this->object->getFixedTextLength());
621  $values->setMaxLength($this->object->getFixedTextLength());
622  }
623 
624  return $form;
625  }
626 
638  protected function populateNumericGapFormPart($form, $gap, $gapCounter): ilPropertyFormGUI
639  {
640  // #8944: the js-based ouput in self-assessment cannot support formulas
641  if (!$this->object->getSelfAssessmentEditingMode()) {
642  $value = new ilFormulaInputGUI($this->lng->txt('value'), "gap_" . $gapCounter . "_numeric");
643  $value->setInlineStyle('text-align: right;');
644 
645  $lowerbound = new ilFormulaInputGUI($this->lng->txt('range_lower_limit'), "gap_" . $gapCounter . "_numeric_lower");
646  $lowerbound->setInlineStyle('text-align: right;');
647 
648  $upperbound = new ilFormulaInputGUI($this->lng->txt('range_upper_limit'), "gap_" . $gapCounter . "_numeric_upper");
649  $upperbound->setInlineStyle('text-align: right;');
650  } else {
651  $value = new ilNumberInputGUI($this->lng->txt('value'), "gap_" . $gapCounter . "_numeric");
652  $value->allowDecimals(true);
653 
654  $lowerbound = new ilNumberInputGUI($this->lng->txt('range_lower_limit'), "gap_" . $gapCounter . "_numeric_lower");
655  $lowerbound->allowDecimals(true);
656 
657  $upperbound = new ilNumberInputGUI($this->lng->txt('range_upper_limit'), "gap_" . $gapCounter . "_numeric_upper");
658  $upperbound->allowDecimals(true);
659  }
660 
661  $value->setSize(10);
662  $value->setValue(ilLegacyFormElementsUtil::prepareFormOutput($gap->getAnswertext()));
663  $value->setRequired(true);
664  $form->addItem($value);
665 
666  $lowerbound->setSize(10);
667  $lowerbound->setRequired(true);
668  $lowerbound->setValue(ilLegacyFormElementsUtil::prepareFormOutput($gap->getLowerBound()));
669  $form->addItem($lowerbound);
670 
671  $upperbound->setSize(10);
672  $upperbound->setRequired(true);
673  $upperbound->setValue(ilLegacyFormElementsUtil::prepareFormOutput($gap->getUpperBound()));
674  $form->addItem($upperbound);
675 
676  if ($this->object->getFixedTextLength() > 0) {
677  $value->setSize($this->object->getFixedTextLength());
678  $value->setMaxLength($this->object->getFixedTextLength());
679  $lowerbound->setSize($this->object->getFixedTextLength());
680  $lowerbound->setMaxLength($this->object->getFixedTextLength());
681  $upperbound->setSize($this->object->getFixedTextLength());
682  $upperbound->setMaxLength($this->object->getFixedTextLength());
683  }
684 
685  $points = new ilNumberInputGUI($this->lng->txt('points'), "gap_" . $gapCounter . "_numeric_points");
686  $points->allowDecimals(true);
687  $points->setSize(3);
688  $points->setRequired(true);
689  $points->setValue(ilLegacyFormElementsUtil::prepareFormOutput($gap->getPoints()));
690  $form->addItem($points);
691  return $form;
692  }
693 
697  public function createGaps(): void
698  {
700  $this->writePostData(true);
701  $this->object->saveToDb();
702  $this->editQuestion();
703  }
704 
705  public function removegap(): void
706  {
708  $this->writePostData(true);
709  $this->object->deleteAnswerText(
710  $this->gapIndex,
711  $this->request_data_collector->getCmdIndex('removegap_' . $this->gapIndex)
712  );
713  $this->editQuestion();
714  }
715 
716  public function addgap(): void
717  {
719  $this->writePostData(true);
720  $this->object->addGapAnswer(
721  $this->gapIndex,
722  $this->request_data_collector->getCmdIndex('addgap_' . $this->gapIndex) + 1,
723  ''
724  );
725  $this->editQuestion();
726  }
727 
728  public function getPreview(
729  bool $show_question_only = false,
730  bool $show_inline_feedback = false
731  ): string {
732  $user_solution = is_object($this->getPreviewSession()) ? (array) $this->getPreviewSession()->getParticipantsSolution() : [];
733 
734  $template = new ilTemplate("tpl.il_as_qpl_cloze_question_output.html", true, true, "components/ILIAS/TestQuestionPool");
735  $output = $this->object->getClozeTextForHTMLOutput();
736  foreach ($this->object->getGaps() as $gap_index => $gap) {
737  switch ($gap->getType()) {
739  $gaptemplate = new ilTemplate("tpl.il_as_qpl_cloze_question_gap_text.html", true, true, "components/ILIAS/TestQuestionPool");
740 
741  $gap_size = $gap->getGapSize() > 0 ? $gap->getGapSize() : $this->object->getFixedTextLength();
742  if ($gap_size > 0) {
743  $gaptemplate->setCurrentBlock('size_and_maxlength');
744  $gaptemplate->setVariable("TEXT_GAP_SIZE", $gap_size);
745  $gaptemplate->parseCurrentBlock();
746  }
747  $gaptemplate->setVariable("GAP_COUNTER", $gap_index);
748  foreach ($user_solution as $val1 => $val2) {
749  if (strcmp($val1, $gap_index) == 0) {
750  $gaptemplate->setVariable("VALUE_GAP", " value=\"" . ilLegacyFormElementsUtil::prepareFormOutput(
751  $val2
752  ) . "\"");
753  }
754  }
755  // fau: fixGapReplace - use replace function
756  $output = $this->object->replaceFirstGap($output, $gaptemplate->get());
757  // fau.
758  break;
760  $gaptemplate = new ilTemplate("tpl.il_as_qpl_cloze_question_gap_select.html", true, true, "components/ILIAS/TestQuestionPool");
761  foreach ($gap->getItems($this->object->getShuffler(), $gap_index) as $item) {
762  $gaptemplate->setCurrentBlock("select_gap_option");
763  $gaptemplate->setVariable("SELECT_GAP_VALUE", $item->getOrder());
764  $gaptemplate->setVariable(
765  "SELECT_GAP_TEXT",
766  ilLegacyFormElementsUtil::prepareFormOutput($item->getAnswerText())
767  );
768  foreach ($user_solution as $val1 => $val2) {
769  if (strcmp($val1, $gap_index) == 0) {
770  if (strcmp($val2, $item->getOrder()) == 0) {
771  $gaptemplate->setVariable("SELECT_GAP_SELECTED", " selected=\"selected\"");
772  }
773  }
774  }
775  $gaptemplate->parseCurrentBlock();
776  }
777  $gaptemplate->setVariable("PLEASE_SELECT", $this->lng->txt("please_select"));
778  $gaptemplate->setVariable("GAP_COUNTER", $gap_index);// fau: fixGapReplace - use replace function
779  $output = $this->object->replaceFirstGap($output, $gaptemplate->get());
780  // fau.
781  break;
783  $gaptemplate = new ilTemplate("tpl.il_as_qpl_cloze_question_gap_numeric.html", true, true, "components/ILIAS/TestQuestionPool");
784  $gap_size = $gap->getGapSize() > 0 ? $gap->getGapSize() : $this->object->getFixedTextLength();
785  if ($gap_size > 0) {
786  $gaptemplate->setCurrentBlock('size_and_maxlength');
787  $gaptemplate->setVariable("TEXT_GAP_SIZE", $gap_size);
788  $gaptemplate->parseCurrentBlock();
789  }
790  $gaptemplate->setVariable("GAP_COUNTER", $gap_index);
791  foreach ($user_solution as $val1 => $val2) {
792  if (strcmp($val1, $gap_index) == 0) {
793  $gaptemplate->setVariable("VALUE_GAP", " value=\"" . ilLegacyFormElementsUtil::prepareFormOutput(
794  $val2
795  ) . "\"");
796  }
797  }
798  // fau: fixGapReplace - use replace function
799  $output = $this->object->replaceFirstGap($output, $gaptemplate->get());
800  // fau.
801  break;
802  }
803  }
804  $template->setVariable("QUESTIONTEXT", $this->renderLatex($this->object->getQuestionForHTMLOutput()));
805  $template->setVariable("CLOZETEXT", $this->renderLatex(ilLegacyFormElementsUtil::prepareTextareaOutput($output, true)));
806  $questionoutput = $template->get();
807  if (!$show_question_only) {
808  // get page object output
809  $questionoutput = $this->getILIASPage($questionoutput);
810  }
811  return $questionoutput;
812  }
813 
814  public function getSolutionOutput(
815  int $active_id,
816  ?int $pass = null,
817  bool $graphical_output = false,
818  bool $result_output = false,
819  bool $show_question_only = true,
820  bool $show_feedback = false,
821  bool $show_correct_solution = false,
822  bool $show_manual_scoring = false,
823  bool $show_question_text = true,
824  bool $show_inline_feedback = true
825  ): string {
826  $user_solution = [];
827  if (($active_id > 0) && (!$show_correct_solution)) {
828  $user_solution = $this->object->getSolutionValues($active_id, $pass);
829  if (!is_array($user_solution)) {
830  $user_solution = [];
831  }
832  }
833 
834  return $this->renderSolutionOutput(
835  $user_solution,
836  $active_id,
837  $pass,
838  $graphical_output,
839  $result_output,
840  $show_question_only,
841  $show_feedback,
842  $show_correct_solution,
843  $show_manual_scoring,
844  $show_question_text,
845  false,
846  false
847  );
848  }
849 
850  public function renderSolutionOutput(
851  mixed $user_solutions,
852  int $active_id,
853  ?int $pass,
854  bool $graphical_output = false,
855  bool $result_output = false,
856  bool $show_question_only = true,
857  bool $show_feedback = false,
858  bool $show_correct_solution = false,
859  bool $show_manual_scoring = false,
860  bool $show_question_text = true,
861  bool $show_autosave_title = false,
862  bool $show_inline_feedback = false,
863  ): ?string {
864  $template = new ilTemplate("tpl.il_as_qpl_cloze_question_output_solution.html", true, true, "components/ILIAS/TestQuestionPool");
865  $output = $this->object->getClozeTextForHTMLOutput();
866  $assClozeGapCombinationObject = new assClozeGapCombination($this->db);
867  $check_for_gap_combinations = $assClozeGapCombinationObject->loadFromDb($this->object->getId());
868 
869  foreach ($this->object->getGaps() as $gap_index => $gap) {
870  $gaptemplate = new ilTemplate("tpl.il_as_qpl_cloze_question_output_solution_gap.html", true, true, "components/ILIAS/TestQuestionPool");
871  $found = [];
872  foreach ($user_solutions as $solutionarray) {
873  if ($solutionarray["value1"] == $gap_index) {
874  $found = $solutionarray;
875  }
876  }
877 
878  if ($active_id
879  && $graphical_output) {
880  // output of ok/not ok icons for user entered solutions
881  $details = $this->object->getUserResultDetails($active_id, $pass);
882  $check = $details[$gap_index] ?? [];
883 
884  if (count($check_for_gap_combinations) != 0) {
885  $gaps_used_in_combination = $assClozeGapCombinationObject->getGapsWhichAreUsedInCombination($this->object->getId());
886  $custom_user_solution = [];
887  if (array_key_exists($gap_index, $gaps_used_in_combination)) {
888  $combination_id = $gaps_used_in_combination[$gap_index];
889  foreach ($gaps_used_in_combination as $key => $value) {
890  $a = 0;
891  if ($value == $combination_id) {
892  foreach ($user_solutions as $solution_key => $solution_value) {
893  if ($solution_value['value1'] == $key) {
894  $result_row = [];
895  $result_row['gap_id'] = $solution_value['value1'];
896  $result_row['value'] = $solution_value['value2'];
897  array_push($custom_user_solution, $result_row);
898  }
899  }
900  }
901  }
902  $points_array = $this->object->calculateCombinationResult($custom_user_solution);
903  $max_combination_points = $assClozeGapCombinationObject->getMaxPointsForCombination($this->object->getId(), $combination_id);
904  if ($points_array[0] == $max_combination_points) {
905  $gaptemplate->setVariable("ICON_OK", $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_OK));
906  } elseif ($points_array[0] > 0) {
907  $gaptemplate->setVariable("ICON_OK", $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_MOSTLY_OK));
908  } else {
909  $gaptemplate->setVariable("ICON_OK", $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_NOT_OK));
910  }
911  } else {
912  if (array_key_exists('best', $check) && $check["best"]) {
913  $gaptemplate->setCurrentBlock("icon_ok");
914  $gaptemplate->setVariable("ICON_OK", $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_OK));
915  $gaptemplate->parseCurrentBlock();
916  } else {
917  $gaptemplate->setCurrentBlock("icon_ok");
918  if (array_key_exists('positive', $check) && $check["positive"]) {
919  $gaptemplate->setVariable("ICON_OK", $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_MOSTLY_OK));
920  } else {
921  $gaptemplate->setVariable("ICON_OK", $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_NOT_OK));
922  }
923  $gaptemplate->parseCurrentBlock();
924  }
925  }
926  } else {
927  if (array_key_exists('best', $check) && $check["best"]) {
928  $gaptemplate->setCurrentBlock("icon_ok");
929  $gaptemplate->setVariable("ICON_OK", $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_OK));
930  $gaptemplate->parseCurrentBlock();
931  } else {
932  $gaptemplate->setCurrentBlock("icon_ok");
933  if (array_key_exists('positive', $check) && $check["positive"]) {
934  $gaptemplate->setVariable("ICON_OK", $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_MOSTLY_OK));
935  } else {
936  $gaptemplate->setVariable("ICON_OK", $this->generateCorrectnessIconsForCorrectness(self::CORRECTNESS_NOT_OK));
937  }
938  $gaptemplate->parseCurrentBlock();
939  }
940  }
941  }
942 
943  switch ($gap->getType()) {
946  $solutiontext = "";
947  if (($active_id > 0) && (!$show_correct_solution)) {
948  if ((count($found) == 0) || (strlen(trim($found["value2"])) == 0)) {
949  for ($chars = 0; $chars < $gap->getMaxWidth(); $chars++) {
950  $solutiontext .= "&nbsp;";
951  }
952  } else {
953  $solutiontext = ilLegacyFormElementsUtil::prepareFormOutput($found["value2"]);
954  }
955  } else {
956  $solutiontext = $this-> getBestSolutionText($gap, $gap_index, $check_for_gap_combinations);
957  }
958  $this->populateSolutiontextToGapTpl($gaptemplate, $gap, $solutiontext);
959  // fau: fixGapReplace - use replace function
960  $output = $this->object->replaceFirstGap($output, $gaptemplate->get());
961  // fau.
962  break;
964  $solutiontext = "";
965  if (($active_id > 0) && (!$show_correct_solution)) {
966  if ((count($found) == 0) || (strlen(trim($found["value2"])) == 0)) {
967  for ($chars = 0; $chars < $gap->getMaxWidth(); $chars++) {
968  $solutiontext .= "&nbsp;";
969  }
970  } else {
971  $item = $gap->getItem($found["value2"]);
972  if (is_object($item)) {
973  $solutiontext = ilLegacyFormElementsUtil::prepareFormOutput($item->getAnswertext());
974  } else {
975  for ($chars = 0; $chars < $gap->getMaxWidth(); $chars++) {
976  $solutiontext .= "&nbsp;";
977  }
978  }
979  }
980  } else {
981  $solutiontext = $this-> getBestSolutionText($gap, $gap_index, $check_for_gap_combinations);
982  }
983  $this->populateSolutiontextToGapTpl($gaptemplate, $gap, $solutiontext);
984  // fau: fixGapReplace - use replace function
985  $output = $this->object->replaceFirstGap($output, $gaptemplate->get());
986  // fau.
987  break;
988  }
989  }
990 
991  if ($show_question_text) {
992  $template->setVariable("QUESTIONTEXT", $this->renderLatex($this->object->getQuestionForHTMLOutput()));
993  }
994 
995  $template->setVariable("CLOZETEXT", $this->renderLatex(ilLegacyFormElementsUtil::prepareTextareaOutput($output, true)));
996 
997  // generate the question output
998  $solutiontemplate = new ilTemplate("tpl.il_as_tst_solution_output.html", true, true, "components/ILIAS/TestQuestionPool");
999  $questionoutput = $template->get();
1000 
1001  $feedback = '';
1002  if ($show_feedback) {
1003  if (!$this->isTestPresentationContext()) {
1004  $feedback = $this->getGenericFeedbackOutput((int) $active_id, $pass);
1005  }
1006 
1007  $feedback = $this->getSpecificFeedbackOutput(
1008  $this->object->fetchIndexedValuesFromValuePairs($user_solutions)
1009  );
1010  }
1011  if ($feedback !== '') {
1012  $cssClass = (
1013  $this->hasCorrectSolution($active_id, $pass) ?
1015  );
1016 
1017  $solutiontemplate->setVariable("ILC_FB_CSS_CLASS", $cssClass);
1018  $solutiontemplate->setVariable("FEEDBACK", $this->renderLatex(
1020  ));
1021  }
1022 
1023  $solutiontemplate->setVariable("SOLUTION_OUTPUT", $questionoutput);
1024 
1025  $solutionoutput = $solutiontemplate->get();
1026 
1027  if (!$show_question_only) {
1028  // get page object output
1029  $solutionoutput = $this->getILIASPage($solutionoutput);
1030  }
1031 
1032  return $solutionoutput;
1033  }
1034 
1041  protected function getBestSolutionText($gap, $gap_index, $gap_combinations): string
1042  {
1043  $combination = null;
1044  foreach ((array) $gap_combinations as $combiGapSolRow) {
1045  if ($combiGapSolRow['gap_fi'] == $gap_index && $combiGapSolRow['best_solution']) {
1046  $combination = $combiGapSolRow;
1047  break;
1048  }
1049  }
1050  $best_solution_text = ilLegacyFormElementsUtil::prepareFormOutput(
1051  $gap->getBestSolutionOutput(
1052  $this->object->getShuffler(),
1053  $combination
1054  )
1055  );
1056  return $best_solution_text;
1057  }
1058 
1059  public function getGenericFeedbackOutput(int $active_id, $pass): string
1060  {
1061  $manual_feedback = ilObjTest::getManualFeedback($active_id, $this->object->getId(), $pass);
1062  if (strlen($manual_feedback)) {
1063  return $manual_feedback;
1064  }
1065  $correct_feedback = $this->object->feedbackOBJ->getGenericFeedbackTestPresentation($this->object->getId(), true);
1066  $incorrect_feedback = $this->object->feedbackOBJ->getGenericFeedbackTestPresentation($this->object->getId(), false);
1067 
1068  $output = '';
1069  if ($correct_feedback . $incorrect_feedback !== '') {
1070  $output = $this->genericFeedbackOutputBuilder($active_id, $pass);
1071  }
1072  //$test = new ilObjTest($this->object->active_id);
1073  return $this->renderLatex(ilLegacyFormElementsUtil::prepareTextareaOutput($output, true));
1074  }
1075 
1076  public function getTestOutput(
1077  int $active_id,
1078  int $pass,
1079  bool $is_question_postponed = false,
1080  array|bool $user_post_solutions = false,
1081  bool $show_specific_inline_feedback = false
1082  ): string {
1083  // get the solution of the user for the active pass or from the last pass if allowed
1084  $user_solution = [];
1085  if ($user_post_solutions !== false) {
1086  $indexedSolution = $this->object->fetchSolutionSubmit();
1087  $user_solution = $this->object->fetchValuePairsFromIndexedValues($indexedSolution);
1088  } elseif ($active_id) {
1089  $user_solution = $this->object->getTestOutputSolutions($active_id, $pass);
1090  // hey.
1091  if (!is_array($user_solution)) {
1092  $user_solution = [];
1093  }
1094  }
1095 
1096  $template = new ilTemplate("tpl.il_as_qpl_cloze_question_output.html", true, true, "components/ILIAS/TestQuestionPool");
1097  $output = $this->object->getClozeTextForHTMLOutput();
1098  foreach ($this->object->getGaps() as $gap_index => $gap) {
1099  switch ($gap->getType()) {
1101  $gaptemplate = new ilTemplate("tpl.il_as_qpl_cloze_question_gap_text.html", true, true, "components/ILIAS/TestQuestionPool");
1102  $gap_size = $gap->getGapSize() > 0 ? $gap->getGapSize() : $this->object->getFixedTextLength();
1103 
1104  if ($gap_size > 0) {
1105  $gaptemplate->setCurrentBlock('size_and_maxlength');
1106  $gaptemplate->setVariable("TEXT_GAP_SIZE", $gap_size);
1107  $gaptemplate->parseCurrentBlock();
1108  }
1109 
1110  $gaptemplate->setVariable("GAP_COUNTER", $gap_index);
1111  foreach ($user_solution as $solution) {
1112  if (strcmp($solution["value1"], $gap_index) == 0) {
1113  $gaptemplate->setVariable("VALUE_GAP", " value=\"" . ilLegacyFormElementsUtil::prepareFormOutput(
1114  $solution["value2"]
1115  ) . "\"");
1116  }
1117  }
1118  // fau: fixGapReplace - use replace function
1119  $output = $this->object->replaceFirstGap($output, $gaptemplate->get());
1120  // fau.
1121  break;
1123  $gaptemplate = new ilTemplate("tpl.il_as_qpl_cloze_question_gap_select.html", true, true, "components/ILIAS/TestQuestionPool");
1124  foreach ($gap->getItems($this->object->getShuffler(), $gap_index) as $item) {
1125  $gaptemplate->setCurrentBlock("select_gap_option");
1126  $gaptemplate->setVariable("SELECT_GAP_VALUE", $item->getOrder());
1127  $gaptemplate->setVariable(
1128  "SELECT_GAP_TEXT",
1129  ilLegacyFormElementsUtil::prepareFormOutput($item->getAnswerText())
1130  );
1131  foreach ($user_solution as $solution) {
1132  if (strcmp($solution["value1"], $gap_index) == 0) {
1133  if (strcmp($solution["value2"], $item->getOrder()) == 0) {
1134  $gaptemplate->setVariable("SELECT_GAP_SELECTED", " selected=\"selected\"");
1135  }
1136  }
1137  }
1138  $gaptemplate->parseCurrentBlock();
1139  }
1140  $gaptemplate->setVariable("PLEASE_SELECT", $this->lng->txt("please_select"));
1141  $gaptemplate->setVariable("GAP_COUNTER", $gap_index);// fau: fixGapReplace - use replace function
1142  $output = $this->object->replaceFirstGap($output, $gaptemplate->get());
1143  // fau.
1144  break;
1146  $gaptemplate = new ilTemplate("tpl.il_as_qpl_cloze_question_gap_numeric.html", true, true, "components/ILIAS/TestQuestionPool");
1147  $gap_size = $gap->getGapSize() > 0 ? $gap->getGapSize() : $this->object->getFixedTextLength();
1148  if ($gap_size > 0) {
1149  $gaptemplate->setCurrentBlock('size_and_maxlength');
1150  $gaptemplate->setVariable("TEXT_GAP_SIZE", $gap_size);
1151  $gaptemplate->parseCurrentBlock();
1152  }
1153 
1154  $gaptemplate->setVariable("GAP_COUNTER", $gap_index);
1155  foreach ($user_solution as $solution) {
1156  if (strcmp($solution["value1"], $gap_index) == 0) {
1157  $gaptemplate->setVariable("VALUE_GAP", " value=\"" . ilLegacyFormElementsUtil::prepareFormOutput(
1158  $solution["value2"]
1159  ) . "\"");
1160  }
1161  }
1162  // fau: fixGapReplace - use replace function
1163  $output = $this->object->replaceFirstGap($output, $gaptemplate->get());
1164  // fau.
1165  break;
1166  }
1167  }
1168 
1169  $template->setVariable("QUESTIONTEXT", $this->renderLatex($this->object->getQuestionForHTMLOutput()));
1170  $template->setVariable("CLOZETEXT", $this->renderLatex(ilLegacyFormElementsUtil::prepareTextareaOutput($output, true)));
1171  $questionoutput = $template->get();
1172  $pageoutput = $this->outQuestionPage("", $is_question_postponed, $active_id, $questionoutput);
1173  return $pageoutput;
1174  }
1175 
1176  public function getSpecificFeedbackOutput(array $user_solution): string
1177  {
1178  if (!$this->object->feedbackOBJ->isSpecificAnswerFeedbackAvailable($this->object->getId())) {
1179  return '';
1180  }
1181 
1182  $feedback = '<table class="test_specific_feedback"><tbody>';
1183 
1184  foreach ($this->object->gaps as $gap_index => $gap) {
1185  $answer_value = $this->object->fetchAnswerValueForGap($user_solution, $gap_index);
1186  if ($gap->getType() !== assClozeGap::TYPE_TEXT
1187  && $answer_value === '') {
1188  continue;
1189  }
1190  $answer_index = $this->object->feedbackOBJ->determineAnswerIndexForAnswerValue($gap, $answer_value);
1191  $fb = $this->object->feedbackOBJ->determineTestOutputGapFeedback($gap_index, $answer_index);
1192 
1193  $caption = $this->lng->txt('gap') . ' ' . ($gap_index + 1) . ': ';
1194  $feedback .= '<tr><td>';
1195  $feedback .= $caption . '</td><td>';
1196  $feedback .= $fb . '</td> </tr>';
1197  }
1198  $feedback .= '</tbody></table>';
1199 
1200  return $this->renderLatex(ilLegacyFormElementsUtil::prepareTextareaOutput($feedback, true));
1201  }
1202 
1213  {
1214  return [];
1215  }
1216 
1227  {
1228  return [];
1229  }
1230 
1237  public function getAggregatedAnswersView(array $relevant_answers): string
1238  {
1239  $overview = [];
1240  $aggregation = [];
1241  foreach ($relevant_answers as $answer) {
1242  $overview[$answer['active_fi']][$answer['pass']][$answer['value1']] = $answer['value2'];
1243  }
1244 
1245  foreach ($overview as $active) {
1246  foreach ($active as $answer) {
1247  foreach ($answer as $option => $value) {
1248  $aggregation[$option][$value] = $aggregation[$option][$value] + 1;
1249  }
1250  }
1251  }
1252 
1253  $html = '<div>';
1254  $i = 0;
1255  foreach ($this->object->getGaps() as $gap_index => $gap) {
1256  if ($gap->type == assClozeGap::TYPE_SELECT) {
1257  $html .= '<p>Gap ' . ($i + 1) . ' - SELECT</p>';
1258  $html .= '<ul>';
1259  $j = 0;
1260  foreach ($gap->getItems($this->object->getShuffler(), $gap_index) as $gap_item) {
1261  $aggregate = $aggregation[$i];
1262  $html .= '<li>' . $gap_item->getAnswerText() . ' - ' . ($aggregate[$j] ? $aggregate[$j] : 0) . '</li>';
1263  $j++;
1264  }
1265  $html .= '</ul>';
1266  }
1267 
1268  if ($gap->type == assClozeGap::TYPE_TEXT) {
1269  $present_elements = [];
1270  foreach ($gap->getItems($this->randomGroup->shuffleArray(new Seed\RandomSeed())) as $item) {
1272  $present_elements[] = $item->getAnswertext();
1273  }
1274 
1275  $html .= '<p>Gap ' . ($i + 1) . ' - TEXT</p>';
1276  $html .= '<ul>';
1277  $aggregate = (array) $aggregation[$i];
1278  foreach ($aggregate as $answer => $count) {
1279  $show_mover = '';
1280  if (in_array($answer, $present_elements)) {
1281  $show_mover = ' style="display: none;" ';
1282  }
1283 
1284  $html .= '<li>' . $answer . ' - ' . $count
1285  . '&nbsp;<button class="clone_fields_add btn btn-link" ' . $show_mover . ' data-answer="' . $answer . '" name="add_gap_' . $i . '_0">
1286  <span class="sr-only"></span><span class="glyphicon glyphicon-plus"></span></button>
1287  </li>';
1288  }
1289  $html .= '</ul>';
1290  }
1291 
1292  if ($gap->type == assClozeGap::TYPE_NUMERIC) {
1293  $html .= '<p>Gap ' . ($i + 1) . ' - NUMERIC</p>';
1294  $html .= '<ul>';
1295  $j = 0;
1296  foreach ($gap->getItems($this->object->getShuffler(), $gap_index) as $gap_item) {
1297  $aggregate = (array) $aggregation[$i];
1298  foreach ($aggregate as $answer => $count) {
1299  $html .= '<li>' . $answer . ' - ' . $count . '</li>';
1300  }
1301  $j++;
1302  }
1303  $html .= '</ul>';
1304  }
1305  $i++;
1306  $html .= '<hr />';
1307  }
1308 
1309  $html .= '</div>';
1310  return $html;
1311  }
1312 
1313  public function applyIndizesToGapText($question_text): string
1314  {
1315  $parts = explode('[gap', $question_text);
1316  $i = 0;
1317  $question_text = '';
1318  foreach ($parts as $part) {
1319  if ($i == 0) {
1320  $question_text .= $part;
1321  } else {
1322  $question_text .= '[gap ' . $i . $part;
1323  }
1324  $i++;
1325  }
1326  return $question_text;
1327  }
1328 
1329  public function removeIndizesFromGapText($question_text): string
1330  {
1331  $parts = preg_split('/\[gap \d*\]/', $question_text);
1332  $question_text = implode('[gap]', $parts);
1333  return $question_text;
1334  }
1335 
1340  private function populateSolutiontextToGapTpl($gaptemplate, $gap, $solutiontext): void
1341  {
1342  if ($this->isRenderPurposePrintPdf()) {
1343  $gaptemplate->setCurrentBlock('gap_span');
1344  $gaptemplate->setVariable('SPAN_SOLUTION', $solutiontext);
1345  } elseif ($gap->getType() == assClozeGap::TYPE_SELECT) {
1346  $gaptemplate->setCurrentBlock('gap_select');
1347  $gaptemplate->setVariable('SELECT_SOLUTION', $solutiontext);
1348  } else {
1349  $gap_size = $gap->getGapSize() > 0 ? $gap->getGapSize() : $this->object->getFixedTextLength();
1350 
1351  if ($gap_size > 0) {
1352  $gaptemplate->setCurrentBlock('gap_size');
1353  $gaptemplate->setVariable("GAP_SIZE", $gap_size);
1354  $gaptemplate->parseCurrentBlock();
1355  }
1356 
1357  $gaptemplate->setCurrentBlock('gap_input');
1358  $gaptemplate->setVariable('INPUT_SOLUTION', $solutiontext);
1359  }
1360 
1361 
1362  $gaptemplate->parseCurrentBlock();
1363  }
1364 
1365  protected function hasAddAnswerAction($relevantAnswers, $questionIndex): bool
1366  {
1367  foreach ($this->getAnswersFrequency($relevantAnswers, $questionIndex) as $answer) {
1368  if (isset($answer['actions'])) {
1369  return true;
1370  }
1371  }
1372 
1373  return false;
1374  }
1375 
1376  public function getAnswerFrequencyTableGUI($parentGui, $parentCmd, $relevantAnswers, $questionIndex): ilAnswerFrequencyStatisticTableGUI
1377  {
1378  $table = parent::getAnswerFrequencyTableGUI(
1379  $parentGui,
1380  $parentCmd,
1381  $relevantAnswers,
1382  $questionIndex
1383  );
1384 
1385  $table->setTitle(
1386  sprintf(
1387  $this->lng->txt('tst_corrections_answers_tbl_subindex'),
1388  $this->lng->txt('gap') . ' ' . ($questionIndex + 1)
1389  )
1390  );
1391 
1392  if ($this->hasAddAnswerAction($relevantAnswers, $questionIndex)) {
1393  $table->addColumn('', '', '200');
1394  }
1395 
1396  return $table;
1397  }
1398 
1399  public function getSubQuestionsIndex(): array
1400  {
1401  return array_keys($this->object->getGaps());
1402  }
1403 
1404  protected function getAnswerTextLabel($gapIndex, $answer)
1405  {
1406  $gap = $this->object->getGap($gapIndex);
1407 
1408  switch ($gap->type) {
1411  return $answer;
1412 
1414  default:
1415  $items = $gap->getItems($this->randomGroup->dontShuffle());
1416  return $items[$answer]->getAnswertext();
1417  }
1418  }
1419 
1420  protected function completeAddAnswerAction($answers, $gap_index): array
1421  {
1422  $gap = $this->object->getGap($gap_index);
1423 
1424  if ($gap->type != assClozeGap::TYPE_TEXT ||
1425  $this->isUsedInCombinations($gap_index)) {
1426  return $answers;
1427  }
1428 
1429  foreach ($answers as $key => $ans) {
1430  $found = false;
1431 
1432  foreach ($gap->getItems($this->randomGroup->dontShuffle()) as $item) {
1433  if ($ans['answer'] !== $item->getAnswerText()) {
1434  continue;
1435  }
1436 
1437  $found = true;
1438  break;
1439  }
1440 
1441  if (!$found) {
1442  $answers[$key]['addable'] = true;
1443  }
1444  }
1445 
1446  return $answers;
1447  }
1448 
1449  public function getAnswersFrequency($relevantAnswers, $questionIndex): array
1450  {
1451  $answers = [];
1452 
1453  foreach ($relevantAnswers as $row) {
1454  if ($row['value1'] != $questionIndex) {
1455  continue;
1456  }
1457 
1458  if (!isset($answers[$row['value2']])) {
1459  $label = $this->getAnswerTextLabel($row['value1'], $row['value2']);
1460 
1461  $answers[$row['value2']] = [
1462  'answer' => $label, 'frequency' => 0
1463  ];
1464  }
1465 
1466  $answers[$row['value2']]['frequency']++;
1467  }
1468 
1469  $answers = $this->completeAddAnswerAction($answers, $questionIndex);
1470 
1471  return $answers;
1472  }
1473 
1474  protected function isUsedInCombinations($gapIndex): bool
1475  {
1476  foreach ($this->object->getGapCombinations() as $combination) {
1477  if ($combination['gap_fi'] != $gapIndex) {
1478  continue;
1479  }
1480 
1481  return true;
1482  }
1483 
1484  return false;
1485  }
1486 
1487  protected function getGapCombinations(): array
1488  {
1489  $combinations = [];
1490 
1491  foreach ($this->object->getGapCombinations() as $c) {
1492  if (!isset($combinations[$c['cid']])) {
1493  $combinations[$c['cid']] = [];
1494  }
1495 
1496  if (!isset($combinations[$c['cid']][$c['row_id']])) {
1497  $combinations[$c['cid']][$c['row_id']] = [
1498  'gaps' => [], 'points' => $c['points'],
1499  ];
1500  }
1501 
1502  if (!isset($combinations[$c['cid']][$c['row_id']]['gaps'][$c['gap_fi']])) {
1503  $combinations[$c['cid']][$c['row_id']]['gaps'][$c['gap_fi']] = [];
1504  }
1505 
1506  $combinations[$c['cid']][$c['row_id']]['gaps'][$c['gap_fi']] = $c['answer'];
1507  }
1508 
1509  return $combinations;
1510  }
1511 
1513  {
1514  foreach ($this->object->getGaps() as $gapIndex => $gap) {
1516  $form,
1517  $gap,
1518  $gapIndex,
1519  $this->isUsedInCombinations($gapIndex)
1520  );
1521  }
1522 
1523  if ($this->object->getGapCombinationsExists()) {
1524  foreach ($this->getGapCombinations() as $combiIndex => $gapCombination) {
1525  $this->populateGapCombinationCorrectionFormProperty($form, $gapCombination, $combiIndex);
1526  }
1527  }
1528  }
1529 
1530  protected function populateGapCombinationCorrectionFormProperty(ilPropertyFormGUI $form, $gapCombi, $combiIndex): void
1531  {
1532  $header = new ilFormSectionHeaderGUI();
1533  $header->setTitle("Gap Combination " . ($combiIndex + 1));
1534  $form->addItem($header);
1535 
1536  $inp = new ilAssClozeTestCombinationVariantsInputGUI('Answers', 'combination_' . $combiIndex);
1537  $inp->setValues($gapCombi);
1538  $form->addItem($inp);
1539  }
1540 
1546  protected function populateGapCorrectionFormProperties($form, $gap, $gapIndex, $hidePoints): void
1547  {
1548  $header = new ilFormSectionHeaderGUI();
1549  $header->setTitle($this->lng->txt("gap") . " " . ($gapIndex + 1));
1550  $form->addItem($header);
1551 
1552  if ($gap->getType() == assClozeGap::TYPE_TEXT || $gap->getType() == assClozeGap::TYPE_SELECT) {
1553  $this->populateTextOrSelectGapCorrectionFormProperty($form, $gap, $gapIndex, $hidePoints);
1554  } elseif ($gap->getType() == assClozeGap::TYPE_NUMERIC) {
1555  foreach ($gap->getItemsRaw() as $item) {
1556  $this->populateNumericGapCorrectionFormProperty($form, $item, $gapIndex, $hidePoints);
1557  }
1558  }
1559  }
1560 
1561  protected function populateTextOrSelectGapCorrectionFormProperty($form, $gap, $gapIndex, $hidePoints): void
1562  {
1563  $values = new ilAssAnswerCorrectionsInputGUI($this->lng->txt("values"), "gap_" . $gapIndex);
1564  $values->setHidePointsEnabled($hidePoints);
1565  $values->setRequired(true);
1566  $values->setQuestionObject($this->object);
1567  $values->setValues($gap->getItemsRaw());
1568  $form->addItem($values);
1569  }
1570 
1571  protected function populateNumericGapCorrectionFormProperty($form, $item, $gapIndex, $hidePoints): void
1572  {
1573  $value = new ilNumberInputGUI($this->lng->txt('value'), "gap_" . $gapIndex . "_numeric");
1574  $value->allowDecimals(true);
1575  $value->setSize(10);
1576  $value->setValue(ilLegacyFormElementsUtil::prepareFormOutput($item->getAnswertext()));
1577  $value->setRequired(true);
1578  $form->addItem($value);
1579 
1580  $lowerbound = new ilNumberInputGUI($this->lng->txt('range_lower_limit'), "gap_" . $gapIndex . "_numeric_lower");
1581  $lowerbound->allowDecimals(true);
1582  $lowerbound->setSize(10);
1583  $lowerbound->setRequired(true);
1584  $lowerbound->setValue(ilLegacyFormElementsUtil::prepareFormOutput($item->getLowerBound()));
1585  $form->addItem($lowerbound);
1586 
1587  $upperbound = new ilNumberInputGUI($this->lng->txt('range_upper_limit'), "gap_" . $gapIndex . "_numeric_upper");
1588  $upperbound->allowDecimals(true);
1589  $upperbound->setSize(10);
1590  $upperbound->setRequired(true);
1591  $upperbound->setValue(ilLegacyFormElementsUtil::prepareFormOutput($item->getUpperBound()));
1592  $form->addItem($upperbound);
1593 
1594  if (!$hidePoints) {
1595  $points = new ilNumberInputGUI($this->lng->txt('points'), "gap_" . $gapIndex . "_numeric_points");
1596  $points->allowDecimals(true);
1597  $points->setSize(3);
1598  $points->setRequired(true);
1599  $points->setValue(ilLegacyFormElementsUtil::prepareFormOutput($item->getPoints()));
1600  $form->addItem($points);
1601  }
1602  }
1603 
1608  {
1609  foreach ($this->object->getGaps() as $gapIndex => $gap) {
1610  if ($this->isUsedInCombinations($gapIndex)) {
1611  continue;
1612  }
1613 
1614  $this->saveGapCorrectionFormProperty($form, $gap, $gapIndex);
1615  }
1616 
1617  if ($this->object->getGapCombinationsExists()) {
1619  }
1620  }
1621 
1623  {
1624  if ($gap->getType() == assClozeGap::TYPE_TEXT || $gap->getType() == assClozeGap::TYPE_SELECT) {
1626  } elseif ($gap->getType() == assClozeGap::TYPE_NUMERIC) {
1627  foreach ($gap->getItemsRaw() as $item) {
1628  $this->saveNumericGapCorrectionFormProperty($form, $item, $gapIndex);
1629  }
1630  }
1631  }
1632 
1634  {
1635  $answers = $form->getItemByPostVar('gap_' . $gapIndex)->getValues();
1636 
1637  foreach ($gap->getItemsRaw() as $index => $item) {
1638  $item->setPoints((float) str_replace(',', '.', $answers[$index]->getPoints()));
1639  }
1640  }
1641 
1643  {
1644  $item->setAnswertext($form->getInput('gap_' . $gapIndex . '_numeric'));
1645  $item->setLowerBound($form->getInput('gap_' . $gapIndex . '_numeric_lower'));
1646  $item->setUpperBound($form->getInput('gap_' . $gapIndex . '_numeric_upper'));
1647  $item->setPoints((float) str_replace(',', '.', $form->getInput('gap_' . $gapIndex . '_numeric_points')));
1648  }
1649 
1651  {
1652  // please dont ask (!) -.-
1653 
1654  $combinationPoints = ['points' => [], 'select' => []];
1655  $combinationValues = [];
1656 
1657  foreach ($this->getGapCombinations() as $combiId => $combi) {
1658  $values = $form->getItemByPostVar('combination_' . $combiId)->getValues();
1659 
1660  if (!isset($combinationPoints['points'][$combiId])) {
1661  $combinationPoints['points'][$combiId] = [];
1662  $combinationPoints['select'][$combiId] = [];
1663  $combinationValues[$combiId] = [];
1664  }
1665 
1666  foreach ($combi as $varId => $variant) {
1667  $combinationPoints['points'][$combiId][$varId] = (float) str_replace(',', '.', $values[$varId]['points']);
1668  $combinationPoints['select'][$combiId] = array_keys($values[$varId]['gaps']);
1669  $combinationValues[$combiId][$varId] = array_values($values[$varId]['gaps']);
1670  }
1671  }
1672 
1673  $assClozeGapCombinationObject = new assClozeGapCombination($this->db);
1674  $assClozeGapCombinationObject->clearGapCombinationsFromDb($this->object->getId());
1675 
1676  $assClozeGapCombinationObject->saveGapCombinationToDb(
1677  $this->object->getId(),
1678  $combinationPoints,
1679  $combinationValues
1680  );
1681  $this->object->setGapCombinationsExists(true);
1682  }
1683 }
const ADDITIONAL_CONTENT_EDITING_MODE_IPE
This class represents a formula text property in a property form.
static getSelfAssessmentTags()
Get tags allowed in question tags in self assessment mode.
hasCorrectSolution($activeId, $passIndex)
genericFeedbackOutputBuilder(int $active_id, ?int $pass)
populateNumericGapCorrectionFormProperty($form, $item, $gapIndex, $hidePoints)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
generateCorrectnessIconsForCorrectness(int $correctness)
getAnswersFrequency($relevantAnswers, $questionIndex)
getTestOutput(int $active_id, int $pass, bool $is_question_postponed=false, array|bool $user_post_solutions=false, bool $show_specific_inline_feedback=false)
Class for cloze tests.
getAfterParticipationSuppressionAnswerPostVars()
Returns a list of postvars which will be suppressed in the form output when used in scoring adjustmen...
This class represents a selection list property in a property form.
getItemByPostVar(string $a_post_var)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
if($clientAssertionType !='urn:ietf:params:oauth:client-assertion-type:jwt-bearer'|| $grantType !='client_credentials') $parts
Definition: ltitoken.php:61
populateCorrectionsFormProperties(ilPropertyFormGUI $form)
renderLatex($content)
Wrap content with latex in a LatexContent UI component and render it to be processed by MathJax in th...
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)
saveCorrectionsFormProperties(ilPropertyFormGUI $form)
saveGapCorrectionFormProperty(ilPropertyFormGUI $form, assClozeGap $gap, $gapIndex)
setPoints($points=0.0)
Sets the points.
addBasicQuestionFormProperties(ilPropertyFormGUI $form)
parseCurrentBlock(string $blockname=self::DEFAULT_BLOCK)
Parses the given block.
escapeTemplatePlaceholders(string $text)
getAnswerFrequencyTableGUI($parentGui, $parentCmd, $relevantAnswers, $questionIndex)
saveGapCombinationCorrectionFormProperties(ilPropertyFormGUI $form)
Class for cloze question gaps.
setOptions(array $a_options)
populateTextGapFormPart($form, $gap, $gapCounter)
Populates the form-part for a text gap.
populateSolutiontextToGapTpl($gaptemplate, $gap, $solutiontext)
writeQuestionSpecificPostData(ilPropertyFormGUI $form)
Extracts the question specific values from the request and applies them to the data object...
ilGlobalPageTemplate $tpl
getPreview(bool $show_question_only=false, bool $show_inline_feedback=false)
static prepareFormOutput($a_str, bool $a_strip=false)
$c
Definition: deliver.php:25
populateTaxonomyFormSection(ilPropertyFormGUI $form)
getInput(string $a_post_var, bool $ensureValidation=true)
Returns the input of an item, if item provides getInput method and as fallback the value of the HTTP-...
editQuestion(bool $checkonly=false, ?bool $is_save_cmd=null)
Creates an output of the edit form for the question.
getBestSolutionText($gap, $gap_index, $gap_combinations)
addQuestionFormCommandButtons(ilPropertyFormGUI $form)
getAfterParticipationSuppressionQuestionPostVars()
Returns a list of postvars which will be suppressed in the form output when used in scoring adjustmen...
This class represents a single choice wizard property in a property form.
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
allowDecimals(bool $a_value)
static _getUsedHTMLTags(string $module='')
populateGapCombinationCorrectionFormProperty(ilPropertyFormGUI $form, $gapCombi, $combiIndex)
This class represents a hidden form property in a property form.
getItemsRaw()
Gets the items of a cloze gap.
Cloze test question GUI representation.
writeAnswerSpecificPostData(ilPropertyFormGUI $form)
Extracts the answer specific values from the request and applies them to the data object...
populateQuestionSpecificFormPart(ilPropertyFormGUI $form)
Adds the question specific forms parts to a question property form gui.
createGaps()
Create gaps from cloze text.
get(string $part=self::DEFAULT_BLOCK)
Renders the given block and returns the html string.
hasAddAnswerAction($relevantAnswers, $questionIndex)
populateGapFormPart($form, $gapCounter)
Populates a gap form-part.
This class represents a number property in a property form.
setValue(?string $a_value)
populateAnswerSpecificFormPart(ilPropertyFormGUI $form)
Adds the answer specific form parts to a question property form gui.
populateTextOrSelectGapCorrectionFormProperty($form, $gap, $gapIndex, $hidePoints)
global $DIC
Definition: shib_login.php:26
saveTextOrSelectGapCorrectionFormProperty(ilPropertyFormGUI $form, assClozeGap $gap, $gapIndex)
hasErrorInGapCombinationPoints(array $gap_combinations)
populateGapSizeFormPart($form, $gap, $gapCounter)
writePostData(bool $always=false)
{}
setRequired(bool $a_required)
saveNumericGapCorrectionFormProperty(ilPropertyFormGUI $form, assAnswerCloze $item, $gapIndex)
setUpperBound(string $bound)
Sets the upper bound.
populateGapCorrectionFormProperties($form, $gap, $gapIndex, $hidePoints)
populateNumericGapFormPart($form, $gap, $gapCounter)
Populates the form-part for a numeric gap.
populateSelectGapFormPart($form, $gap, $gapCounter)
Populates the form-part for a select gap.
getILIASPage(string $html="")
Returns the ILIAS Page around a question.
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,)
outQuestionPage($a_temp_var, $a_postponed=false, $active_id="", $html="", $inlineFeedbackEnabled=false)
completeAddAnswerAction($answers, $gap_index)
applyIndizesToGapText($question_text)
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
__construct(Container $dic, ilPlugin $plugin)
This class represents a text area property in a property form.
getGenericFeedbackOutput(int $active_id, $pass)
setAnswertext($answertext="")
Sets the answer text.
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
setLowerBound(string $bound)
Sets the lower boind.
$check
Definition: buildRTE.php:81
getAddGapButtonClickClosure(string $gap_type)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$gapIndex
A temporary variable to store gap indexes of ilCtrl commands in the getCommand method.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setInlineStyle(string $a_style)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static getManualFeedback(int $active_id, int $question_id, ?int $pass)
Retrieves the feedback comment for a question in a test if it is finalized.
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...
setVariable(string $variable, $value='')
Sets the given variable to the given value.
getSpecificFeedbackOutput(array $user_solution)
removeIndizesFromGapText($question_text)
__construct(int $id=-1)
assClozeTestGUI constructor
getAnswerTextLabel($gapIndex, $answer)
renderEditForm(ilPropertyFormGUI $form)