ILIAS  release_10 Revision v10.1-43-ga1241a92c2f
class.TestScoringByQuestionGUI.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
26 use ILIAS\Test\Questions\Properties\Properties as TestQuestionProperties;
33 
35 {
36  private const CMD_SHOW = 'showManScoringByQuestionParticipantsTable';
37  private const CMD_SAVE = 'saveManScoringByQuestion';
38 
42 
43  public function __construct(
44  \ilObjTest $object,
45  private readonly \ilUIService $ui_service
46  ) {
47  parent::__construct($object);
48  $this->lng->loadLanguageModule('form');
49 
50  $this->ctrl->saveParameterByClass(self::class, 'q_id');
51  $uri = ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(
52  [\ilObjTestGUI::class, self::class],
53  $this->getDefaultCommand()
54  );
55  $this->ctrl->clearParameterByClass(self::class, 'q_id');
56  $url_builder = new URLBuilder(
57  (new DataFactory())->uri($uri)
58  );
59 
60  list(
61  $this->url_builder,
62  $this->action_parameter_token,
63  $this->row_id_token
64  ) = $url_builder->acquireParameters(
65  ['manual_scoring', 'by_question'],
66  'action',
67  'row'
68  );
69  }
70 
71  protected function getDefaultCommand(): string
72  {
73  return self::CMD_SHOW;
74  }
75 
76  protected function getActiveSubTabId(): string
77  {
78  return 'man_scoring_by_qst';
79  }
80 
82  RoundTripModal $modal = null
83  ): void {
84  $this->tabs->activateTab(TabsManager::TAB_ID_MANUAL_SCORING);
85 
86  $this->initJavascript();
87 
88  if (!$this->test_access->checkScoreParticipantsAccess()
89  || !$this->object->getGlobalSettings()->isManualScoringEnabled()) {
90  $this->tpl->setOnScreenMessage('info', $this->lng->txt('cannot_edit_test'), true);
91  $this->ctrl->redirectByClass([\ilRepositoryGUI::class, \ilObjTestGUI::class, \ilInfoScreenGUI::class]);
92  }
93 
94  $test_question_properties = $this->testquestionsrepository
95  ->getQuestionPropertiesForTest($this->object);
96 
97  if ($test_question_properties === []) {
98  $this->tpl->setOnScreenMessage('info', $this->lng->txt('manscoring_questions_not_found'));
99  return;
100  }
101 
102  $this->toolbar->addComponent(
103  $this->ui_factory->dropdown()->standard(
104  $this->buildSelectableQuestionsArray($test_question_properties)
105  )->withLabel(
106  $this->lng->txt('select_question')
107  )
108  );
109 
110  $question_id = $this->testrequest->getQuestionId();
111  if ($question_id === 0) {
112  $question_id = reset($test_question_properties)->getQuestionId();
113  }
114 
115  $table = new ScoringByQuestionTable(
116  $this->lng,
117  $this->url_builder,
118  $this->action_parameter_token,
119  $this->row_id_token,
120  $this->ui_factory
121  );
122 
123  if ($this->testrequest->strVal($this->action_parameter_token->getName()) === ScoringByQuestionTable::ACTION_SCORING) {
124  $affected_rows = $this->testrequest->raw($this->row_id_token->getName());
125  $this->getAnswerDetail($question_id, $affected_rows[0]);
126  }
127 
128  $content = [
129  $table->getTable(
130  $this->buildQuestionTitleWithPoints($test_question_properties[$question_id]),
131  $this->user->getDateTimeFormat(),
132  $this->http->request(),
133  $this->ui_service,
134  $this->ctrl->getLinkTargetByClass(
135  [\ilObjTestGUI::class, self::class],
136  $this->getDefaultCommand()
137  ),
139  $this->lng,
140  new \DateTimeZone($this->user->getTimeZone()),
141  $this->participant_access_filter,
142  $this->object,
143  $question_id
144  )
145  )
146  ];
147 
148  if ($modal !== null) {
149  $content[] = $modal->withOnLoad($modal->getShowSignal());
150  }
151 
152  $this->tpl->setContent($this->ui_renderer->render($content));
153  }
154 
155  protected function saveManScoringByQuestion(): void
156  {
157  $active_id = $this->testrequest->getActiveId();
158  $question_id = $this->testrequest->getQuestionId();
159  $attempt = $this->testrequest->getPassId();
160  if ($active_id === 0 || $question_id === 0
161  || !$this->test_access->checkScoreParticipantsAccessForActiveId($active_id, $this->object->getTestId())) {
162  $this->tpl->setOnScreenMessage('info', $this->lng->txt('cannot_edit_test'), true);
163  $this->ctrl->redirectByClass(\ilObjTestGUI::class);
164  }
165 
166  $question_gui = $this->object->createQuestionGUI('', $question_id);
167  $previously_reached_points = $question_gui->getObject()->getReachedPoints($active_id, $attempt);
168  $available_points = $question_gui->getObject()->getMaximumPoints();
169  $feedback = \ilObjTest::getSingleManualFeedback($active_id, $question_id, $attempt);
170  $form = $this->buildForm(
171  $attempt,
172  $active_id,
173  $question_id,
174  $previously_reached_points,
175  $available_points
176  );
177 
178  if (!$form->checkInput()) {
179  $form->setValuesByPost();
181  $this->buildFeedbackModal(
182  $question_id,
183  $active_id,
184  $attempt,
185  $form
186  )
187  );
188  return;
189  }
190 
191  if (isset($feedback['finalized_evaluation'])
192  && $feedback['finalized_evaluation'] === 1) {
193  $new_reached_points = $previously_reached_points;
194  $feedback_text = $feedback['feedback'];
195  } else {
196  $new_reached_points = $this->refinery->kindlyTo()->float()
197  ->transform($form->getInput('points'));
198  $feedback_text = \ilUtil::stripSlashes(
199  $form->getInput('feedback'),
200  false,
202  );
203  }
204  if ($new_reached_points !== $previously_reached_points) {
205  \assQuestion::_setReachedPoints(
206  $active_id,
207  $question_id,
208  $new_reached_points,
209  $available_points,
210  $attempt,
211  true
212  );
214  $this->object->getId(),
216  );
217  }
218 
219  $finalized = $this->refinery->byTrying([
220  $this->refinery->kindlyTo()->bool(),
221  $this->refinery->always(false)
222  ])->transform($form->getInput('finalized'));
223  $this->object->saveManualFeedback(
224  $active_id,
225  $question_id,
226  $attempt,
227  $feedback_text,
228  $finalized
229  );
230 
231  if ($this->logger->isLoggingEnabled()) {
232  $this->logger->logScoringInteraction(
233  $this->logger->getInteractionFactory()->buildScoringInteraction(
234  $this->getObject()->getRefId(),
235  $question_id,
236  $this->user->getId(),
238  TestScoringInteractionTypes::QUESTION_GRADED,
239  [
243  ->getAdditionalInformationGenerator()->getTrueFalseTagForBool($finalized)
244  ]
245  )
246  );
247  }
248 
249  $this->tpl->setOnScreenMessage(
250  'success',
251  sprintf(
252  $this->lng->txt('tst_saved_manscoring_by_question_successfully'),
253  $question_gui->getObject()->getTitleForHTMLOutput(),
254  $attempt + 1
255  )
256  );
258  }
259 
260  protected function getAnswerDetail(int $question_id, string $row_id): void
261  {
262  $row_info_array = explode('_', $row_id);
263 
264  if (count($row_info_array) !== 2) {
265  $this->http->close();
266  }
267 
268  [$active_id, $attempt] = $this->refinery->container()->mapValues(
269  $this->refinery->kindlyTo()->int()
270  )->transform($row_info_array);
271 
272  if (!$this->getTestAccess()->checkScoreParticipantsAccessForActiveId($active_id, $this->object->getTestId())) {
273  $this->http->close();
274  }
275 
276  $this->http->saveResponse(
277  $this->http->response()->withBody(
279  $this->ui_renderer->renderAsync(
280  $this->buildFeedbackModal($question_id, $active_id, $attempt)
281  )
282  )
283  )->withHeader(\ILIAS\HTTP\Response\ResponseHeader::CONTENT_TYPE, 'text/html')
284  );
285  $this->http->sendResponse();
286  $this->http->close();
287  }
288 
289  private function buildFeedbackModal(
290  int $question_id,
291  int $active_id,
292  int $attempt,
293  \ilPropertyFormGUI $form = null
294  ): RoundTripModal {
295  $question_gui = $this->object->createQuestionGUI('', $question_id);
296 
297  $content = [$this->buildSolutionPanel($question_gui, $active_id, $attempt)];
298 
299  if ($this->object->getAutosave()
300  && $autosave_content = $this->buildAutosavedSolutionPanel($question_gui, $active_id, $attempt)
301  ) {
302  $content[] = $autosave_content;
303  }
304 
305  $reached_points = $question_gui->getObject()->getReachedPoints($active_id, $attempt);
306  $available_points = $question_gui->getObject()->getMaximumPoints();
307  $content[] = $this->ui_factory->panel()->standard(
308  $this->lng->txt('scoring'),
309  $this->ui_factory->legacy(
310  sprintf(
311  $this->lng->txt('part_received_a_of_b_points'),
312  $reached_points,
313  $available_points
314  )
315  )
316  );
317 
318  $suggested_solution = \assQuestion::_getSuggestedSolutionOutput($question_id);
319  if ($this->object->getShowSolutionSuggested() && $suggested_solution !== '') {
320  $content[] = $this->ui_factory->legacy(
321  $this->ui_factory->panel()->standard(
322  $this->lng->txt('solution_hint'),
323  $suggested_solution
324  )
325  );
326  }
327 
328  $content[] = $this->ui_factory->legacy(($form ?? $this->buildForm(
329  $attempt,
330  $active_id,
331  $question_id,
332  $reached_points,
333  $available_points
334  ))->getHTMLAsync());
335 
336  return $this->ui_factory->modal()->roundtrip(
337  $this->getModalTitle($active_id),
338  $content
339  );
340  }
341 
342  private function buildSolutionPanel(
343  \assQuestionGUI $question_gui,
344  int $active_id,
345  int $attempt
346  ): StandardPanel {
347  return $this->ui_factory->panel()->standard(
348  $question_gui->getObject()->getTitleForHTMLOutput(),
349  $this->ui_factory->legacy(
350  $question_gui->getSolutionOutput(
351  $active_id,
352  $attempt,
353  false,
354  false,
355  false,
356  $this->object->getShowSolutionFeedback(),
357  )
358  )
359  );
360  }
361 
362  private function buildAutosavedSolutionPanel(
363  \assQuestionGUI $question_gui,
364  int $active_id,
365  int $attempt
366  ): ?StandardPanel {
367  $autosave_content = $question_gui->getAutoSavedSolutionOutput(
368  $active_id,
369  $attempt,
370  false,
371  false,
372  false,
373  $this->object->getShowSolutionFeedback(),
374  );
375  if ($autosave_content === null) {
376  return null;
377  }
378 
379  return $this->ui_factory->panel()->standard(
380  $this->lng->txt('autosavecontent'),
381  $this->ui_factory->legacy($autosave_content)
382  );
383  }
384 
385  private function getModalTitle(int $active_id): string
386  {
387  if ($this->object->getAnonymity() === true) {
388  return $this->lng->txt('answers_of') . ' ' . $this->lng->txt('anonymous');
389  }
390  return $this->lng->txt('answers_of') . ' ' . $this->object->getCompleteEvaluationData()
391  ->getParticipant($active_id)
392  ->getName();
393  }
394 
395  private function buildForm(
396  int $attempt,
397  int $active_id,
398  int $question_id,
399  float $reached_points,
400  float $available_points
401  ): \ilPropertyFormGUI {
402  $feedback = \ilObjTest::getSingleManualFeedback($active_id, $question_id, $attempt);
403  $finalized = isset($feedback['finalized_evaluation'])
404  && $feedback['finalized_evaluation'] === 1;
405 
406  $form = new \ilPropertyFormGUI();
407  $form->setFormAction($this->buildFormTarget($question_id, $active_id, $attempt));
408  $form->setTitle($this->lng->txt('manscoring'));
409  $form->addCommandButton(self::CMD_SAVE, $this->lng->txt('save'));
410  $form->setId('fb');
411 
412  if ($finalized) {
413  $feedback_input = new \ilNonEditableValueGUI(
414  $this->lng->txt('set_manual_feedback'),
415  'feedback',
416  true
417  );
418  } else {
419  $feedback_input = new \ilTextAreaInputGUI(
420  $this->lng->txt('set_manual_feedback'),
421  'feedback'
422  );
423  $feedback_input->setUseRte(true);
424  }
425  $feedback_input->setValue($feedback['feedback'] ?? '');
426  $form->addItem($feedback_input);
427 
428  $reached_points_input = new \ilNumberInputGUI(
429  $this->lng->txt('tst_change_points_for_question'),
430  'points'
431  );
432  $reached_points_input->allowDecimals(true);
433  $reached_points_input->setSize(5);
434  $reached_points_input->setMaxValue($available_points, true);
435  $reached_points_input->setMinValue(0);
436  $reached_points_input->setDisabled($finalized);
437  $reached_points_input->setValue((string) $reached_points);
438  $reached_points_input->setClientSideValidation(true);
439  $form->addItem($reached_points_input);
440 
441  $finalized_input = new \ilCheckboxInputGUI(
442  $this->lng->txt('finalized_evaluation'),
443  'finalized'
444  );
445  $finalized_input->setChecked($finalized);
446  $form->addItem($finalized_input);
447 
448  return $form;
449  }
450 
451  protected function buildFormTarget(
452  int $question_id,
453  int $active_id,
454  int $attempt
455  ): string {
456  $this->ctrl->setParameterByClass(self::class, 'q_id', $question_id);
457  $this->ctrl->setParameterByClass(self::class, 'active_id', $active_id);
458  $this->ctrl->setParameterByClass(self::class, 'pass_id', $attempt);
459  $target = $this->ctrl->getFormAction($this, self::CMD_SAVE);
460  $this->ctrl->clearParameterByClass(self::class, 'q_id');
461  $this->ctrl->clearParameterByClass(self::class, 'active_id');
462  $this->ctrl->clearParameterByClass(self::class, 'pass_id');
463  return $target;
464  }
465 
471  private function buildSelectableQuestionsArray(array $question_data): array
472  {
473  $dropdown = array_map(
474  function (TestQuestionProperties $v): StandardLink {
475  $this->ctrl->setParameterByClass(self::class, 'q_id', $v->getGeneralQuestionProperties()->getQuestionId());
476  return $this->ui_factory->link()->standard(
477  $this->buildQuestionTitleWithPoints($v),
478  $this->ctrl->getLinkTargetByClass(self::class, $this->getDefaultCommand())
479  );
480  },
481  $question_data
482  );
483  $this->ctrl->clearParameterByClass(self::class, 'q_id');
484  return $dropdown;
485  }
486 
487  private function buildQuestionTitleWithPoints(TestQuestionProperties $test_question_properties): string
488  {
489  $question_properties = $test_question_properties->getGeneralQuestionProperties();
490  $lang_var = $question_properties->getAvailablePoints() === 1.0 ? $this->lng->txt('point') : $this->lng->txt('points');
491  return "{$this->refinery->encode()->htmlSpecialCharsAsEntities()->transform($question_properties->getTitle())} "
492  . "({$question_properties->getAvailablePoints()} {$lang_var}) "
493  . "[{$this->lng->txt('question_id_short')}: {$question_properties->getQuestionId()}]";
494  }
495 
496  private function initJavascript(): void
497  {
498  $math_jax_setting = new \ilSetting('MathJax');
499  if ($math_jax_setting->get('enable')) {
500  $this->tpl->addJavaScript($math_jax_setting->get('path_to_mathjax'));
501  }
502 
503  if (\ilObjAdvancedEditing::_getRichTextEditor() === 'tinymce') {
504  $this->initTinymce();
505  }
506  }
507 
508  private function initTinymce(): void
509  {
510  $this->tpl->addJavaScript('node_modules/tinymce/tinymce.min.js');
511  $this->tpl->addOnLoadCode("
512  const aO = (o) => {
513  o.observe(
514  document.getElementById('ilContentContainer'),
515  {childList: true, subtree: true}
516  );
517  }
518  const o = new MutationObserver(
519  (ml, o) => {
520  o.disconnect();
521  tinymce.remove();
522  tinymce.init({
523  selector: 'textarea.RTEditor',
524  branding: false,
525  height: 250,
526  fix_list_elements: true,
527  statusbar: false,
528  menubar: false,
529  plugins: 'lists',
530  toolbar: 'bold italic underline strikethrough | undo redo | bullist numlist',
531  toolbar_mode: 'sliding',
532  init_instance_callback: () => {aO(o);}
533  });
534  }
535  );
536  aO(o);
537  ");
538  }
539 }
buildAutosavedSolutionPanel(\assQuestionGUI $question_gui, int $active_id, int $attempt)
getAutoSavedSolutionOutput(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)
static _getParticipantId($active_id)
Get user id for active id.
static _getRichTextEditor()
Returns the identifier for the Rich Text Editor.
static _getSuggestedSolutionOutput(int $question_id)
Interface Observer Contains several chained tasks and infos about them.
static stripSlashes(string $a_str, bool $a_strip_html=true, string $a_allow="")
acquireParameters(array $namespace, string ... $names)
Definition: URLBuilder.php:137
buildQuestionTitleWithPoints(TestQuestionProperties $test_question_properties)
buildSolutionPanel(\assQuestionGUI $question_gui, int $active_id, int $attempt)
static getSingleManualFeedback(int $active_id, int $question_id, int $pass)
static http()
Fetches the global http state from ILIAS.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _getUsedHTMLTagsAsString(string $a_module="")
Returns a string of all allowed HTML tags for text editing.
static ofString(string $string)
Creates a new stream with an initial value.
Definition: Streams.php:41
buildFeedbackModal(int $question_id, int $active_id, int $attempt, \ilPropertyFormGUI $form=null)
__construct(\ilObjTest $object, private readonly \ilUIService $ui_service)
__construct(Container $dic, ilPlugin $plugin)
buildFormTarget(int $question_id, int $active_id, int $attempt)
URLBuilder.
Definition: URLBuilder.php:39
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)
buildForm(int $attempt, int $active_id, int $question_id, float $reached_points, float $available_points)
static _updateStatus(int $a_obj_id, int $a_usr_id, ?object $a_obj=null, bool $a_percentage=false, bool $a_force_raise=false)