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