ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.TestScoringByQuestionGUI.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
26use ILIAS\Test\Questions\Properties\Properties as TestQuestionProperties;
27use ILIAS\UI\Component\Link\Standard as StandardLink;
28use ILIAS\UI\Component\Panel\Standard as StandardPanel;
29use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal;
32use ILIAS\Data\Factory as DataFactory;
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');
57 (new DataFactory())->uri($uri)
58 );
59
60 list(
61 $this->url_builder,
62 $this->action_parameter_token,
63 $this->row_id_token
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 ),
140 new ScoringByQuestionTableBinder(
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();
183 $this->showManScoringByQuestionParticipantsTable(
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 );
260 $this->showManScoringByQuestionParticipantsTable();
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
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
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->setDisabled($finalized);
439 $reached_points_input->setValue((string) $reached_points);
440 $reached_points_input->setClientSideValidation(true);
441 $form->addItem($reached_points_input);
442
443 $finalized_input = new \ilCheckboxInputGUI(
444 $this->lng->txt('finalized_evaluation'),
445 'finalized'
446 );
447 $finalized_input->setChecked($finalized);
448 $form->addItem($finalized_input);
449
450 return $form;
451 }
452
453 protected function buildFormTarget(
454 int $question_id,
455 int $active_id,
456 int $attempt
457 ): string {
458 $this->ctrl->setParameterByClass(self::class, 'q_id', $question_id);
459 $this->ctrl->setParameterByClass(self::class, 'active_id', $active_id);
460 $this->ctrl->setParameterByClass(self::class, 'pass_id', $attempt);
461 $target = $this->ctrl->getFormAction($this, self::CMD_SAVE);
462 $this->ctrl->clearParameterByClass(self::class, 'q_id');
463 $this->ctrl->clearParameterByClass(self::class, 'active_id');
464 $this->ctrl->clearParameterByClass(self::class, 'pass_id');
465 return $target;
466 }
467
473 private function buildSelectableQuestionsArray(array $question_data): array
474 {
475 $dropdown = array_map(
476 function (TestQuestionProperties $v): StandardLink {
477 $this->ctrl->setParameterByClass(self::class, 'q_id', $v->getGeneralQuestionProperties()->getQuestionId());
478 return $this->ui_factory->link()->standard(
479 $this->buildQuestionTitleWithPoints($v),
480 $this->ctrl->getLinkTargetByClass(self::class, $this->getDefaultCommand())
481 );
482 },
483 $question_data
484 );
485 $this->ctrl->clearParameterByClass(self::class, 'q_id');
486 return $dropdown;
487 }
488
489 private function buildQuestionTitleWithPoints(TestQuestionProperties $test_question_properties): string
490 {
491 $question_properties = $test_question_properties->getGeneralQuestionProperties();
492 $lang_var = $question_properties->getAvailablePoints() === 1.0 ? $this->lng->txt('point') : $this->lng->txt('points');
493 return "{$this->refinery->encode()->htmlSpecialCharsAsEntities()->transform($question_properties->getTitle())} "
494 . "({$question_properties->getAvailablePoints()} {$lang_var}) "
495 . "[{$this->lng->txt('question_id_short')}: {$question_properties->getQuestionId()}]";
496 }
497
498 private function initJavascript(): void
499 {
500 $math_jax_setting = new \ilSetting('MathJax');
501 if ($math_jax_setting->get('enable')) {
502 $this->tpl->addJavaScript($math_jax_setting->get('path_to_mathjax'));
503 }
504
505 if ((new \ilRTESettings($this->lng, $this->user))->getRichTextEditor() === 'tinymce') {
506 $this->initTinymce();
507 }
508 }
509
510 private function initTinymce(): void
511 {
512 $this->tpl->addJavaScript('node_modules/tinymce/tinymce.min.js');
513 $this->tpl->addOnLoadCode("
514 const aO = (o) => {
515 o.observe(
516 document.getElementById('ilContentContainer'),
517 {childList: true, subtree: true}
518 );
519 }
520 const o = new MutationObserver(
521 (ml, o) => {
522 o.disconnect();
523 tinymce.remove();
524 tinymce.init({
525 selector: 'textarea.RTEditor',
526 branding: false,
527 height: 250,
528 fix_list_elements: true,
529 statusbar: false,
530 menubar: false,
531 plugins: 'lists',
532 toolbar: 'bold italic underline strikethrough | undo redo | bullist numlist',
533 toolbar_mode: 'sliding',
534 init_instance_callback: () => {aO(o);}
535 });
536 }
537 );
538 aO(o);
539 ");
540 }
541}
Builds data types.
Definition: Factory.php:36
static ofString(string $string)
Creates a new stream with an initial value.
Definition: Streams.php:41
buildQuestionTitleWithPoints(TestQuestionProperties $test_question_properties)
__construct(\ilObjTest $object, private readonly \ilUIService $ui_service)
buildFormTarget(int $question_id, int $active_id, int $attempt)
buildAutosavedSolutionPanel(assQuestionGUI $question_gui, int $active_id, int $attempt)
buildFeedbackModal(int $question_id, int $active_id, int $attempt, ?\ilPropertyFormGUI $form=null)
buildForm(int $attempt, int $active_id, int $question_id, float $reached_points, float $available_points)
buildSolutionPanel(\assQuestionGUI $question_gui, int $active_id, int $attempt)
acquireParameters(array $namespace, string ... $names)
Definition: URLBuilder.php:138
static _getSuggestedSolutionOutput(int $question_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _updateStatus(int $a_obj_id, int $a_usr_id, ?object $a_obj=null, bool $a_percentage=false, bool $a_force_raise=false)
static _getParticipantId(int $active_id)
Get user id for active id.
static getSingleManualFeedback(int $active_id, int $question_id, int $pass)
static buildExamId($active_id, $pass, $test_obj_id=null)
This class represents a property form user interface.
static _getUsedHTMLTagsAsString(string $module='')
Filter service.
static stripSlashes(string $a_str, bool $a_strip_html=true, string $a_allow="")
return['delivery_method'=> 'php',]
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This describes a Standard Dropdown.
Definition: Standard.php:27
static http()
Fetches the global http state from ILIAS.
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
Interface Observer \BackgroundTasks Contains several chained tasks and infos about them.