ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
ilObjTestSettingsQuestionBehaviour.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
27 
29 {
30  private const DEFAULT_AUTOSAVE_INTERVAL = 30000;
31 
32  public const ANSWER_FIXATION_NONE = 'none';
33  public const ANSWER_FIXATION_ON_INSTANT_FEEDBACK = 'instant_feedback';
34  public const ANSWER_FIXATION_ON_FOLLOWUP_QUESTION = 'followup_question';
35  public const ANSWER_FIXATION_ON_IFB_OR_FUQST = 'ifb_or_fuqst';
36 
37  public function __construct(
38  int $test_id,
39  protected int $question_title_output_mode,
40  protected bool $autosave_enabled,
41  protected int $autosave_interval,
42  protected bool $shuffle_questions,
43  protected bool $question_hints_enabled,
44  protected bool $instant_feedback_points_enabled,
45  protected bool $instant_feedback_generic_enabled,
46  protected bool $instant_feedback_specific_enabled,
47  protected bool $instant_feedback_solution_enabled,
48  protected bool $force_instant_feedback_on_next_question,
49  protected bool $lock_answer_on_instant_feedback,
50  protected bool $lock_answer_on_next_question,
51  protected bool $compulsory_questions_enabled
52  ) {
53  parent::__construct($test_id);
54  }
55 
60  public function toForm(
64  array $environment = null
65  ): FormInput {
66  $inputs['title_output'] = $f->radio($lng->txt('tst_title_output'))
67  ->withOption('0', $lng->txt('tst_title_output_full'))
68  ->withOption('1', $lng->txt('tst_title_output_hide_points'))
69  ->withOption('3', $lng->txt('tst_title_output_only_points'))
70  ->withOption('2', $lng->txt('tst_title_output_no_title'))
72  ->withAdditionalTransformation($refinery->kindlyTo()->int());
73 
74  $inputs['autosave'] = $this->getInputAutosave($lng, $f, $refinery);
75 
76  $inputs['shuffle_questions'] = $f->checkbox(
77  $lng->txt('tst_shuffle_questions'),
78  $lng->txt('tst_shuffle_questions_description')
79  )->withValue($this->getShuffleQuestions());
80 
81  $inputs['offer_hints'] = $f->checkbox(
82  $lng->txt('tst_setting_offer_hints_label'),
83  $lng->txt('tst_setting_offer_hints_info')
85 
86  if ($environment['participant_data_exists']) {
87  $inputs['shuffle_questions'] = $inputs['shuffle_questions']->withDisabled(true);
88  $inputs['offer_hints'] = $inputs['offer_hints']->withDisabled(true);
89  }
90 
91  $inputs['instant_feedback'] = $this->getInputInstantFeedback($lng, $f, $refinery, $environment);
92  $inputs['lock_answers'] = $this->getInputLockAnswers($lng, $f, $refinery, $environment);
93 
94  $inputs['enable_compulsory_questions'] = $f->checkbox(
95  $lng->txt('tst_setting_enable_obligations_label'),
96  $lng->txt('tst_setting_enable_obligations_info')
98 
99  if ($environment['participant_data_exists']) {
100  $inputs['enable_compulsory_questions'] = $inputs['enable_compulsory_questions']->withDisabled(true);
101  }
102 
103  $section = $f->section($inputs, $lng->txt('tst_presentation_properties'));
104  foreach ($this->getConstraintsSectionQuestionBehaviour($lng, $refinery) as $constraint) {
105  $section = $section->withAdditionalTransformation($constraint);
106  }
107 
108  return $section;
109  }
110 
111  private function getInputAutosave(
112  \ilLanguage $lng,
115  ): OptionalGroup {
116  $trafo = $refinery->custom()->transformation(
117  static function (?array $vs): array {
118  if ($vs === null) {
119  return [
120  'autosave_enabled' => false,
121  'autosave_interval' => self::DEFAULT_AUTOSAVE_INTERVAL
122  ];
123  }
124 
125  return [
126  'autosave_enabled' => true,
127  'autosave_interval' => $vs['autosave_interval'] * 1000
128  ];
129  }
130  );
131  $sub_inputs_autosave['autosave_interval'] = $f->numeric($lng->txt('autosave_ival'), $lng->txt('seconds'))
132  ->withRequired(true)
133  ->withAdditionalTransformation($refinery->int()->isGreaterThan(0))
134  ->withValue(
135  $this->getAutosaveInterval() !== 0
136  ? $this->getAutosaveInterval() / 1000
137  : 30
138  );
139 
140  $autosave_input = $f->optionalGroup(
141  $sub_inputs_autosave,
142  $lng->txt('autosave'),
143  $lng->txt('autosave_info')
144  )->withValue(null)
145  ->withAdditionalTransformation($trafo);
146 
147  if (!$this->getAutosaveEnabled()) {
148  return $autosave_input;
149  }
150 
151  return $autosave_input->withValue(
152  [
153  'autosave_interval' => $this->getAutosaveInterval() / 1000
154  ]
155  );
156  }
157 
158  private function getInputInstantFeedback(
159  \ilLanguage $lng,
162  array $environment
163  ): OptionalGroup {
164  $constraint = $refinery->custom()->constraint(
165  fn(?array $vs) => $vs === null
166  || $vs['enabled_feedback_types']['instant_feedback_specific'] === null
167  && $vs['enabled_feedback_types']['instant_feedback_generic'] === null
168  && $vs['enabled_feedback_types']['instant_feedback_points'] === null
169  && $vs['enabled_feedback_types']['instant_feedback_solution'] === null
170  && $vs['feedback_trigger'] === null
171  || (
172  $vs['enabled_feedback_types']['instant_feedback_specific'] === true
173  || $vs['enabled_feedback_types']['instant_feedback_generic'] === true
174  || $vs['enabled_feedback_types']['instant_feedback_points'] === true
175  || $vs['enabled_feedback_types']['instant_feedback_solution'] === true
176  )
177  && $vs['feedback_trigger'] !== '',
178  $lng->txt('select_at_least_one_feedback_type_and_trigger')
179  );
180  $trafo = $refinery->custom()->transformation(
181  static function (?array $vs): array {
182  if ($vs === null) {
183  return [
184  'enabled_feedback_types' => [
185  'instant_feedback_specific' => false,
186  'instant_feedback_generic' => false,
187  'instant_feedback_points' => false,
188  'instant_feedback_solution' => false
189  ],
190  'feedback_on_next_question' => false
191  ];
192  }
193 
194  $vs['feedback_on_next_question'] = $vs['feedback_trigger'] === '1' ? true : false;
195  return $vs;
196  }
197  );
198 
199  $instant_feedback = $f->optionalGroup(
200  $this->getSubInputInstantFeedback($lng, $f, $environment),
201  $lng->txt('tst_instant_feedback'),
202  $lng->txt('tst_instant_feedback_desc')
203  )->withValue(null)
204  ->withAdditionalTransformation($constraint)
205  ->withAdditionalTransformation($trafo);
206 
207  if ($this->isAnyInstantFeedbackOptionEnabled()) {
208  $instant_feedback = $instant_feedback->withValue(
209  [
210  'enabled_feedback_types' => [
211  'instant_feedback_specific' => (bool) $this->getInstantFeedbackSpecificEnabled(),
212  'instant_feedback_generic' => (bool) $this->getInstantFeedbackGenericEnabled(),
213  'instant_feedback_points' => (bool) $this->getInstantFeedbackPointsEnabled(),
214  'instant_feedback_solution' => (bool) $this->getInstantFeedbackSolutionEnabled()
215  ],
216  'feedback_trigger' => (string) ($this->getForceInstantFeedbackOnNextQuestion() ?
217  '1' : '0')
218  ]
219  );
220  }
221 
222  if (!$environment['participant_data_exists']) {
223  return $instant_feedback;
224  }
225 
226  return $instant_feedback->withDisabled(true);
227  }
228 
229  private function getSubInputInstantFeedback(
230  \ilLanguage $lng,
232  array $environment
233  ): array {
234  $feedback_options = [
235  'instant_feedback_points' => $f->checkbox(
236  $lng->txt('tst_instant_feedback_results'),
237  $lng->txt('tst_instant_feedback_results_desc')
238  ),
239  'instant_feedback_generic' => $f->checkbox(
240  $lng->txt('tst_instant_feedback_answer_generic'),
241  $lng->txt('tst_instant_feedback_answer_generic_desc')
242  ),
243  'instant_feedback_specific' => $f->checkbox(
244  $lng->txt('tst_instant_feedback_answer_specific'),
245  $lng->txt('tst_instant_feedback_answer_specific_desc')
246  ),
247  'instant_feedback_solution' => $f->checkbox(
248  $lng->txt('tst_instant_feedback_solution'),
249  $lng->txt('tst_instant_feedback_solution_desc')
250  )
251  ];
252 
253  $sub_inputs_feedback['enabled_feedback_types'] = $f->group(
254  $feedback_options,
255  $lng->txt('tst_instant_feedback_contents')
256  );
257 
258  $sub_inputs_feedback['feedback_trigger'] = $f->radio(
259  $lng->txt('tst_instant_feedback_trigger')
260  )->withOption(
261  '0',
262  $lng->txt('tst_instant_feedback_trigger_manual'),
263  $lng->txt('tst_instant_feedback_trigger_manual_desc')
264  )->withOption(
265  '1',
266  $lng->txt('tst_instant_feedback_trigger_forced'),
267  $lng->txt('tst_instant_feedback_trigger_forced_desc')
268  );
269 
270  if (!$environment['participant_data_exists']) {
271  $sub_inputs_feedback['feedback_trigger'] = $sub_inputs_feedback['feedback_trigger']
272  ->withRequired(true);
273  }
274  return $sub_inputs_feedback;
275 
276 
277  }
278 
279  private function getInputLockAnswers(
280  \ilLanguage $lng,
283  array $environment
284  ): Radio {
285  $lock_answers = $f->radio(
286  $lng->txt('tst_answer_fixation_handling')
287  )->withOption(
288  self::ANSWER_FIXATION_NONE,
289  $lng->txt('tst_answer_fixation_none'),
290  $lng->txt('tst_answer_fixation_none_desc')
291  )->withOption(
292  self::ANSWER_FIXATION_ON_INSTANT_FEEDBACK,
293  $lng->txt('tst_answer_fixation_on_instant_feedback'),
294  $lng->txt('tst_answer_fixation_on_instant_feedback_desc')
295  )->withOption(
296  self::ANSWER_FIXATION_ON_FOLLOWUP_QUESTION,
297  $lng->txt('tst_answer_fixation_on_followup_question'),
298  $lng->txt('tst_answer_fixation_on_followup_question_desc')
299  )->withOption(
300  self::ANSWER_FIXATION_ON_IFB_OR_FUQST,
301  $lng->txt('tst_answer_fixation_on_instantfb_or_followupqst'),
302  $lng->txt('tst_answer_fixation_on_instantfb_or_followupqst_desc')
303  )->withValue(
306 
307  if (!$environment['participant_data_exists']) {
308  return $lock_answers;
309  }
310 
311  return $lock_answers->withDisabled(true);
312  }
313 
315  {
316  return $refinery->custom()->transformation(
317  static function (?string $v): array {
318  if ($v === null || $v === self::ANSWER_FIXATION_NONE) {
319  return [
320  'lock_answer_on_instant_feedback' => false,
321  'lock_answer_on_next_question' => false
322  ];
323  }
324 
325  if ($v === self::ANSWER_FIXATION_ON_INSTANT_FEEDBACK) {
326  return [
327  'lock_answer_on_instant_feedback' => true,
328  'lock_answer_on_next_question' => false
329  ];
330  }
331  if ($v === self::ANSWER_FIXATION_ON_FOLLOWUP_QUESTION) {
332  return [
333  'lock_answer_on_instant_feedback' => false,
334  'lock_answer_on_next_question' => true
335  ];
336  }
337 
338  return [
339  'lock_answer_on_instant_feedback' => true,
340  'lock_answer_on_next_question' => true
341  ];
342  }
343  );
344  }
345 
347  \ilLanguage $lng,
349  ): Generator {
350  yield from [
351  $refinery->custom()->constraint(
352  function ($vs): bool {
353  if ($vs['enable_compulsory_questions'] === true
354  && (
355  $vs['lock_answers']['lock_answer_on_instant_feedback']
356  || $vs['lock_answers']['lock_answer_on_next_question']
357  )
358  ) {
359  return false;
360  }
361  return true;
362  },
363  $lng->txt('tst_settings_conflict_compulsory_and_lock')
364  ),
365  $refinery->custom()->constraint(
366  function ($vs): bool {
367  if ($vs['shuffle_questions'] === true
368  && $vs['lock_answers']['lock_answer_on_next_question']) {
369  return false;
370  }
371  return true;
372  },
373  $lng->txt('tst_settings_conflict_shuffle_and_lock')
374  )
375  ];
376  }
377 
378  public function toStorage(): array
379  {
380  return [
381  'title_output' => ['integer', $this->getQuestionTitleOutputMode()],
382  'autosave' => ['integer', (int) $this->getAutosaveEnabled()],
383  'autosave_ival' => ['integer', $this->getAutosaveInterval()],
384  'shuffle_questions' => ['integer', (int) $this->getShuffleQuestions()],
385  'offer_question_hints' => ['integer', (int) $this->getQuestionHintsEnabled()],
386  'answer_feedback_points' => ['integer', (int) $this->getInstantFeedbackPointsEnabled()],
387  'answer_feedback' => ['integer', (int) $this->getInstantFeedbackGenericEnabled()],
388  'specific_feedback' => ['integer', (int) $this->getInstantFeedbackSpecificEnabled()],
389  'instant_verification' => ['integer', (int) $this->getInstantFeedbackSolutionEnabled()],
390  'force_inst_fb' => ['integer', (int) $this->getForceInstantFeedbackOnNextQuestion()],
391  'inst_fb_answer_fixation' => ['integer', (int) $this->getLockAnswerOnInstantFeedbackEnabled()],
392  'follow_qst_answer_fixation' => ['integer', (int) $this->getLockAnswerOnNextQuestionEnabled()],
393  'obligations_enabled' => ['integer', (int) $this->getCompulsoryQuestionsEnabled()]
394  ];
395  }
396 
397  public function getQuestionTitleOutputMode(): int
398  {
399  return $this->question_title_output_mode;
400  }
401  public function withQuestionTitleOutputMode(int $question_title_output_mode): self
402  {
403  $clone = clone $this;
404  $clone->question_title_output_mode = $question_title_output_mode;
405  return $clone;
406  }
407 
408  public function getAutosaveEnabled(): bool
409  {
410  return $this->autosave_enabled;
411  }
412  public function withAutosaveEnabled(bool $autosave_enabled): self
413  {
414  $clone = clone $this;
415  $clone->autosave_enabled = $autosave_enabled;
416  return $clone;
417  }
418 
419  public function getAutosaveInterval(): int
420  {
421  return $this->autosave_interval;
422  }
423  public function withAutosaveInterval(int $autosave_interval): self
424  {
425  $clone = clone $this;
426  $clone->autosave_interval = $autosave_interval;
427  return $clone;
428  }
429 
430  public function getShuffleQuestions(): bool
431  {
432  return $this->shuffle_questions;
433  }
434  public function withShuffleQuestions(bool $shuffle_questions): self
435  {
436  $clone = clone $this;
437  $clone->shuffle_questions = $shuffle_questions;
438  return $clone;
439  }
440 
441  public function getQuestionHintsEnabled(): bool
442  {
443  return $this->question_hints_enabled;
444  }
445  public function withQuestionHintsEnabled(bool $question_hints_enabled): self
446  {
447  $clone = clone $this;
448  $clone->question_hints_enabled = $question_hints_enabled;
449  return $clone;
450  }
451 
452  public function getInstantFeedbackPointsEnabled(): bool
453  {
454  return $this->instant_feedback_points_enabled;
455  }
456  public function withInstantFeedbackPointsEnabled(bool $instant_feedback_points_enabled): self
457  {
458  $clone = clone $this;
459  $clone->instant_feedback_points_enabled = $instant_feedback_points_enabled;
460  return $clone;
461  }
462  public function getInstantFeedbackGenericEnabled(): bool
463  {
464  return $this->instant_feedback_generic_enabled;
465  }
466  public function withInstantFeedbackGenericEnabled(bool $instant_feedback_generic_enabled): self
467  {
468  $clone = clone $this;
469  $clone->instant_feedback_generic_enabled = $instant_feedback_generic_enabled;
470  return $clone;
471  }
472  public function getInstantFeedbackSpecificEnabled(): bool
473  {
474  return $this->instant_feedback_specific_enabled;
475  }
476  public function withInstantFeedbackSpecificEnabled(bool $instant_feedback_specific_enabled): self
477  {
478  $clone = clone $this;
479  $clone->instant_feedback_specific_enabled = $instant_feedback_specific_enabled;
480  return $clone;
481  }
482  public function getInstantFeedbackSolutionEnabled(): bool
483  {
484  return $this->instant_feedback_solution_enabled;
485  }
486  public function withInstantFeedbackSolutionEnabled(bool $instant_feedback_solution_enabled): self
487  {
488  $clone = clone $this;
489  $clone->instant_feedback_solution_enabled = $instant_feedback_solution_enabled;
490  return $clone;
491  }
492  private function isAnyInstantFeedbackOptionEnabled(): bool
493  {
494  return $this->getInstantFeedbackPointsEnabled()
498  }
499 
501  {
502  return $this->force_instant_feedback_on_next_question;
503  }
504  public function withForceInstantFeedbackOnNextQuestion(bool $force_instant_feedback_on_next_question): self
505  {
506  $clone = clone $this;
507  $clone->force_instant_feedback_on_next_question = $force_instant_feedback_on_next_question;
508  return $clone;
509  }
510 
512  {
513  return $this->lock_answer_on_instant_feedback;
514  }
515  public function getLockAnswerOnNextQuestionEnabled(): bool
516  {
517  return $this->lock_answer_on_next_question;
518  }
519  private function getAnswerFixationSettingsAsFormValue(): string
520  {
523  return self::ANSWER_FIXATION_ON_IFB_OR_FUQST;
524  }
525 
527  return self::ANSWER_FIXATION_ON_INSTANT_FEEDBACK;
528  }
529 
530  if ($this->getLockAnswerOnNextQuestionEnabled()) {
531  return self::ANSWER_FIXATION_ON_FOLLOWUP_QUESTION;
532  }
533 
534  return self::ANSWER_FIXATION_NONE;
535  }
536  public function withLockAnswerOnInstantFeedbackEnabled(bool $lock_answer_on_instant_feedback): self
537  {
538  $clone = clone $this;
539  $clone->lock_answer_on_instant_feedback = $lock_answer_on_instant_feedback;
540  return $clone;
541  }
542  public function withLockAnswerOnNextQuestionEnabled(bool $lock_answer_on_next_question): self
543  {
544  $clone = clone $this;
545  $clone->lock_answer_on_next_question = $lock_answer_on_next_question;
546  return $clone;
547  }
548 
549  public function getCompulsoryQuestionsEnabled(): bool
550  {
551  return $this->compulsory_questions_enabled;
552  }
553  public function withCompulsoryQuestionsEnabled(bool $compulsory_questions_enabled): self
554  {
555  $clone = clone $this;
556  $clone->compulsory_questions_enabled = $compulsory_questions_enabled;
557  return $clone;
558  }
559 }
This is what a factory for input fields looks like.
Definition: Factory.php:28
withInstantFeedbackSpecificEnabled(bool $instant_feedback_specific_enabled)
txt(string $a_topic, string $a_default_lang_fallback_mod="")
gets the text for a given topic if the topic is not in the list, the topic itself with "-" will be re...
withForceInstantFeedbackOnNextQuestion(bool $force_instant_feedback_on_next_question)
This describes optional group inputs.
withLockAnswerOnInstantFeedbackEnabled(bool $lock_answer_on_instant_feedback)
This is what a radio-input looks like.
Definition: Radio.php:28
toForm(\ilLanguage $lng, FieldFactory $f, Refinery $refinery, array $environment=null)
withAdditionalTransformation(Transformation $trafo)
Apply a transformation to the content of the input.
__construct(VocabulariesInterface $vocabularies)
withInstantFeedbackSolutionEnabled(bool $instant_feedback_solution_enabled)
$lng
getInputLockAnswers(\ilLanguage $lng, FieldFactory $f, Refinery $refinery, array $environment)
withLockAnswerOnNextQuestionEnabled(bool $lock_answer_on_next_question)
withCompulsoryQuestionsEnabled(bool $compulsory_questions_enabled)
__construct(int $test_id, protected int $question_title_output_mode, protected bool $autosave_enabled, protected int $autosave_interval, protected bool $shuffle_questions, protected bool $question_hints_enabled, protected bool $instant_feedback_points_enabled, protected bool $instant_feedback_generic_enabled, protected bool $instant_feedback_specific_enabled, protected bool $instant_feedback_solution_enabled, protected bool $force_instant_feedback_on_next_question, protected bool $lock_answer_on_instant_feedback, protected bool $lock_answer_on_next_question, protected bool $compulsory_questions_enabled)
getInputAutosave(\ilLanguage $lng, FieldFactory $f, Refinery $refinery)
withValue($value)
Get an input like this with another value displayed on the client side.
Definition: Group.php:58
getConstraintsSectionQuestionBehaviour(\ilLanguage $lng, Refinery $refinery)
withQuestionTitleOutputMode(int $question_title_output_mode)
withInstantFeedbackGenericEnabled(bool $instant_feedback_generic_enabled)
A transformation is a function from one datatype to another.
getInputInstantFeedback(\ilLanguage $lng, FieldFactory $f, Refinery $refinery, array $environment)
This describes inputs that can be used in forms.
Definition: FormInput.php:31
getSubInputInstantFeedback(\ilLanguage $lng, FieldFactory $f, array $environment)
withInstantFeedbackPointsEnabled(bool $instant_feedback_points_enabled)
Refinery Factory $refinery