ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.TestScoringByParticipantGUI.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
27use ilObjTestGUI;
28
37{
38 public const PART_FILTER_ALL_USERS = 3; // default
41
42 protected \ilTestAccess $test_access;
43
44 public function __construct(\ilObjTest $object)
45 {
46 parent::__construct($object);
47 }
48
52 public function getTestAccess(): \ilTestAccess
53 {
54 return $this->test_access;
55 }
56
60 public function setTestAccess($test_access)
61 {
62 $this->test_access = $test_access;
63 }
64
68 protected function buildSubTabs(string $active_sub_tab = 'man_scoring_by_qst'): void
69 {
70 $this->tabs->addSubTab(
71 'man_scoring_by_qst',
72 $this->lng->txt('tst_man_scoring_by_qst'),
73 $this->ctrl->getLinkTargetByClass([\ilObjTestGUI::class, TestScoringByQuestionGUI::class], 'showManScoringByQuestionParticipantsTable')
74 );
75 $this->tabs->addSubTab(
76 'man_scoring',
77 $this->lng->txt('tst_man_scoring_by_part'),
78 $this->ctrl->getLinkTargetByClass([\ilObjTestGUI::class, self::class], 'showManScoringParticipantsTable')
79 );
80 $this->tabs->setSubTabActive($active_sub_tab);
81 }
82
83 private function fetchActiveIdParameter(): int
84 {
85 if (!$this->testrequest->isset('active_id') || $this->testrequest->int('active_id') === 0) {
86 $this->tpl->setOnScreenMessage('failure', 'no active id given!', true);
87 $this->ctrl->redirectByClass([\ilRepositoryGUI::class, \ilObjTestGUI::class, \ilInfoScreenGUI::class]);
88 }
89
90 return $this->testrequest->int('active_id');
91 }
92
93 private function fetchPassParameter(int $active_id): int
94 {
95 $max_pass = $this->object->_getMaxPass($active_id);
96
97
98 if ($this->testrequest->isset('pass')) {
99 $pass_from_request = $this->testrequest->int('pass');
100 if ($pass_from_request >= 0
101 && $pass_from_request <= $max_pass
102 ) {
103 return $pass_from_request;
104 }
105 }
106
107 if ($this->object->getPassScoring() === \ilObjTest::SCORE_LAST_PASS) {
108 return $max_pass;
109 }
110
111 return $this->object->_getResultPass($active_id);
112 }
113
117 public function executeCommand(): void
118 {
119 if (!$this->getTestAccess()->checkScoreParticipantsAccess()
120 && !$this->getTestAccess()->checkScoreParticipantsAccessAnon()
121 ) {
122 \ilObjTestGUI::accessViolationRedirect();
123 }
124
125 if (!$this->object->getGlobalSettings()->isManualScoringEnabled()) {
126 // allow only if at least one question type is marked for manual scoring
127 $this->tpl->setOnScreenMessage('failure', $this->lng->txt("manscoring_not_allowed"), true);
128 $this->ctrl->redirectByClass([ilRepositoryGUI::class, ilObjTestGUI::class, ilInfoScreenGUI::class]);
129 }
130
131 $this->tabs->activateTab(TabsManager::TAB_ID_MANUAL_SCORING);
132 $this->buildSubTabs($this->getActiveSubTabId());
133
134 $command = $this->ctrl->getCmd($this->getDefaultCommand());
135 $this->$command();
136 }
137
138 protected function getDefaultCommand(): string
139 {
140 return 'manscoring';
141 }
142
143 protected function getActiveSubTabId(): string
144 {
145 return 'man_scoring';
146 }
147
148 private function showManScoringParticipantsTable(): void
149 {
150 $table = $this->buildManScoringParticipantsTable(true);
151 $this->tpl->setContent($table->getHTML());
152 }
153
154 private function applyManScoringParticipantsFilter(): void
155 {
156 $table = $this->buildManScoringParticipantsTable(false);
157 $table->resetOffset();
158 $table->writeFilterToSession();
159
161 }
162
163 private function resetManScoringParticipantsFilter(): void
164 {
165 $table = $this->buildManScoringParticipantsTable(false);
166 $table->resetOffset();
167 $table->resetFilter();
168
170 }
171
172 private function showManScoringParticipantScreen(?\ilPropertyFormGUI $form = null): void
173 {
174 $active_id = $this->fetchActiveIdParameter();
175
176 if (!$this->getTestAccess()->checkScoreParticipantsAccessForActiveId($active_id, $this->object->getTestId())) {
177 \ilObjTestGUI::accessViolationRedirect();
178 }
179
180 $pass = $this->fetchPassParameter($active_id);
181
182 $content_html = '';
183
184 $table = new TestScoringByParticipantPassesOverviewTableGUI($this, 'showManScoringParticipantScreen');
185
186 $user_id = $this->object->_getUserIdFromActiveId($active_id);
187 $user_fullname = $this->object->userLookupFullName($user_id, false, true);
188 $participant_name = $this->getUserNamePresentation(
189 $active_id,
190 $pass,
191 $user_fullname
192 );
193 $table_title = sprintf($this->lng->txt('tst_pass_overview_for_participant'), $participant_name);
194 $table->setTitle($table_title);
195
196 $passOverviewData = $this->service->getPassOverviewData($active_id);
197 $table->setData($passOverviewData['passes']);
198
199 $content_html .= $table->getHTML() . '<br />';
200
201 if ($form === null) {
202 $question_gui_list = $this->getManScoringQuestionGuiList($active_id, $pass);
203 $form = $this->buildManScoringParticipantForm($question_gui_list, $active_id, $pass, true);
204 }
205
206 $content_html .= $form->getHTML();
207
208 $this->tpl->setContent($content_html);
209 }
210
211 private function saveManScoringParticipantScreen(bool $redirect = true): bool
212 {
213 $active_id = $this->fetchActiveIdParameter();
214
215 if (!$this->getTestAccess()->checkScoreParticipantsAccessForActiveId($active_id, $this->object->getTestId())) {
216 \ilObjTestGUI::accessViolationRedirect();
217 }
218
219 $attempt = $this->fetchPassParameter($active_id);
220
221 $question_gui_list = $this->getManScoringQuestionGuiList($active_id, $attempt);
222 $form = $this->buildManScoringParticipantForm($question_gui_list, $active_id, $attempt, false);
223
224 $form->setValuesByPost();
225
226 if (!$form->checkInput()) {
227 $this->tpl->setOnScreenMessage('failure', sprintf($this->lng->txt('tst_save_manscoring_failed'), $attempt + 1));
229 return false;
230 }
231
232 $max_points_by_question_id = [];
233 $max_points_exceeded = false;
234 foreach (array_keys($question_gui_list) as $question_id) {
235 $reached_points = $form->getItemByPostVar("question__{$question_id}__points")->getValue();
236 $max_points = $this->questionrepository->getForQuestionId($question_id)->getAvailablePoints();
237
238 if ($reached_points > $max_points) {
239 $max_points_exceeded = true;
240
241 $form->getItemByPostVar("question__{$question_id}__points")->setAlert(sprintf(
242 $this->lng->txt('tst_manscoring_maxpoints_exceeded_input_alert'),
243 $max_points
244 ));
245 }
246
247 $max_points_by_question_id[$question_id] = $max_points;
248 }
249
250 if ($max_points_exceeded) {
251 $this->tpl->setOnScreenMessage('failure', sprintf($this->lng->txt('tst_save_manscoring_failed'), $attempt + 1));
253 return false;
254 }
255
256 foreach (array_keys($question_gui_list) as $question_id) {
257 $old_points = \assQuestion::_getReachedPoints($active_id, $question_id, $attempt);
258 $reached_points = $this->refinery->byTrying([
259 $this->refinery->kindlyTo()->float(),
260 $this->refinery->always($old_points)
261 ])->transform($form->getItemByPostVar("question__{$question_id}__points")?->getValue());
262
263 $finalized = (bool) $form->getItemByPostVar("{$question_id}__evaluated")?->getChecked();
264 // fix #35543: save manual points only if they differ from the existing points
265 // this prevents a question being set to "answered" if only feedback is entered
266 if ($reached_points !== $old_points) {
267 \assQuestion::_setReachedPoints(
268 $active_id,
269 $question_id,
270 $reached_points,
271 $max_points_by_question_id[$question_id],
272 $attempt,
273 true
274 );
275 }
276
277 $feedback_text = \ilUtil::stripSlashes(
278 (string) $form->getItemByPostVar("question__{$question_id}__feedback")->getValue(),
279 false,
281 );
282
283 $this->object->saveManualFeedback(
284 $active_id,
285 $question_id,
286 $attempt,
287 $feedback_text,
288 $finalized
289 );
290
291 if ($this->logger->isLoggingEnabled()) {
292 $this->logger->getInteractionFactory()->buildScoringInteraction(
293 $this->getObject()->getRefId(),
294 $question_id,
295 $this->user->getId(),
297 TestScoringInteractionTypes::QUESTION_GRADED,
298 [
302 ->getAdditionalInformationGenerator()->getTrueFalseTagForBool(true)
303 ]
304 );
305 }
306
307 $notification_data[$question_id] = [
308 'points' => $reached_points, 'feedback' => $feedback_text
309 ];
310 }
311
313 $this->object->getId(),
315 );
316
317 $manScoringDone = $form->getItemByPostVar("manscoring_done")->getChecked();
318 \ilTestService::setManScoringDone($active_id, $manScoringDone);
319
320 $manScoringNotify = $form->getItemByPostVar("manscoring_notify")->getChecked();
321 if ($manScoringNotify) {
322 $notification = new \ilTestManScoringParticipantNotification(
323 $this->object->_getUserIdFromActiveId($active_id),
324 $this->object->getRefId()
325 );
326
327 $notification->setAdditionalInformation([
328 'test_title' => $this->object->getTitle(),
329 'test_pass' => $attempt + 1,
330 'questions_gui_list' => $question_gui_list,
331 'questions_scoring_data' => $notification_data
332 ]);
333
334 $notification->send();
335 }
336
337 $scorer = new TestScoring($this->object, $this->user, $this->db, $this->test_result_repository);
338 $scorer->setPreserveManualScores(true);
339 $scorer->recalculateSolution($active_id, $attempt);
340
341 if (!$this->object->getAnonymity()
342 && $this->getTestAccess()->checkScoreParticipantsAccess()
343 ) {
345 $name_real_or_anon = $user_name['firstname'] . ' ' . $user_name['lastname'];
346 } else {
347 $name_real_or_anon = $this->lng->txt('anonymous');
348 }
349 $this->tpl->setOnScreenMessage('success', sprintf($this->lng->txt('tst_saved_manscoring_successfully'), $attempt + 1, $name_real_or_anon), true);
350 if ($redirect == true) {
351 $this->ctrl->redirect($this, 'showManScoringParticipantScreen');
352 }
353 return true;
354 }
355
356 private function saveNextManScoringParticipantScreen(): void
357 {
358 $table = $this->buildManScoringParticipantsTable(true);
359
360 if ($this->saveManScoringParticipantScreen(false)) {
361 $participantData = $table->getInternalyOrderedDataValues();
362
363 $nextIndex = null;
364 foreach ($participantData as $index => $participant) {
365 if ($participant['active_id'] == $this->testrequest->raw('active_id')) {
366 $nextIndex = $index + 1;
367 break;
368 }
369 }
370
371 if ($nextIndex && isset($participantData[$nextIndex])) {
372 $this->ctrl->setParameter($this, 'active_id', $participantData[$nextIndex]['active_id']);
373 $this->ctrl->redirect($this, 'showManScoringParticipantScreen');
374 }
375
376 $this->ctrl->redirectByClass(self::class, 'showManScoringParticipantsTable');
377 }
378 }
379
381 {
382 if ($this->saveManScoringParticipantScreen(false)) {
383 $this->ctrl->redirectByClass(self::class, 'showManScoringParticipantsTable');
384 }
385 }
386
388 array $question_gui_list,
389 int $active_id,
390 int $pass,
391 bool $initValues = false
393 $this->ctrl->setParameter($this, 'active_id', $active_id);
394 $this->ctrl->setParameter($this, 'pass', $pass);
395
396 $form = new \ilPropertyFormGUI();
397 $form->setFormAction($this->ctrl->getFormAction($this));
398
399 $form->setTitle(sprintf($this->lng->txt('manscoring_results_pass'), $pass + 1));
400 $form->setTableWidth('100%');
401
402 foreach ($question_gui_list as $question_id => $question_gui) {
403 $question_header = sprintf(
404 $this->lng->txt('tst_manscoring_question_section_header'),
405 $question_gui->getObject()->getTitleForHTMLOutput()
406 );
407 $question_solution = $question_gui->getSolutionOutput($active_id, $pass, false, false, true, false, false, true);
408 $best_solution = $question_gui->getObject()->getSuggestedSolutionOutput();
409
410 $feedback = \ilObjTest::getSingleManualFeedback($active_id, $question_id, $pass);
411
412 $disabled = false;
413 if (isset($feedback['finalized_evaluation']) && $feedback['finalized_evaluation'] == 1) {
414 $disabled = true;
415 }
416
417 $sect = new \ilFormSectionHeaderGUI();
418 $sect->setTitle($question_header . ' [' . $this->lng->txt('question_id_short') . ': ' . $question_gui->getObject()->getId() . ']');
419 $form->addItem($sect);
420
421 $cust = new \ilCustomInputGUI($this->lng->txt('tst_manscoring_input_question_and_user_solution'));
422 $cust->setHtml($question_solution);
423 $form->addItem($cust);
424
425 $number_input_gui = new \ilNumberInputGUI(
426 $this->lng->txt('tst_change_points_for_question'),
427 "question__{$question_id}__points"
428 );
429 $number_input_gui->allowDecimals(true);
430
431 if ($initValues) {
432 $number_input_gui->setValue((string) \assQuestion::_getReachedPoints($active_id, $question_id, $pass));
433 }
434 if ($disabled) {
435 $number_input_gui->setDisabled($disabled);
436 }
437 $form->addItem($number_input_gui);
438
439 $nonedit = new \ilNonEditableValueGUI($this->lng->txt('tst_manscoring_input_max_points_for_question'), "question__{$question_id}__maxpoints");
440 if ($initValues) {
441 $nonedit->setValue($this->questionrepository->getForQuestionId($question_id)->getAvailablePoints());
442 }
443 $form->addItem($nonedit);
444
445 $area = new \ilTextAreaInputGUI($this->lng->txt('set_manual_feedback'), "question__{$question_id}__feedback");
446 $area->setUseRTE(true);
447 if ($initValues) {
448 $area->setValue(\ilObjTest::getSingleManualFeedback((int) $active_id, (int) $question_id, (int) $pass)['feedback'] ?? '');
449 }
450 if ($disabled) {
451 $area->setDisabled($disabled);
452 }
453 $form->addItem($area);
454
455 $check = new \ilCheckboxInputGUI($this->lng->txt('finalized_evaluation'), "{$question_id}__evaluated");
456 if ($disabled) {
457 $check->setChecked(true);
458 }
459 $form->addItem($check);
460
461 if (strlen(trim($best_solution))) {
462 $cust = new \ilCustomInputGUI($this->lng->txt('tst_show_solution_suggested'));
463 $cust->setHtml($best_solution);
464 $form->addItem($cust);
465 }
466 }
467
468 $sect = new \ilFormSectionHeaderGUI();
469 $sect->setTitle($this->lng->txt('tst_participant'));
470 $form->addItem($sect);
471
472 $check = new \ilCheckboxInputGUI($this->lng->txt('set_manscoring_done'), 'manscoring_done');
473 if ($initValues && \ilTestService::isManScoringDone($active_id)) {
474 $check->setChecked(true);
475 }
476 $form->addItem($check);
477
478 $check = new \ilCheckboxInputGUI($this->lng->txt('tst_manscoring_user_notification'), 'manscoring_notify');
479 $form->addItem($check);
480
481 $form->addCommandButton('saveManScoringParticipantScreen', $this->lng->txt('save'));
482 $form->addCommandButton('saveReturnManScoringParticipantScreen', $this->lng->txt('save_return'));
483 $form->addCommandButton('saveNextManScoringParticipantScreen', $this->lng->txt('save_and_next'));
484
485 return $form;
486 }
487
488 private function getManScoringQuestionGuiList(int $active_id, int $pass): array
489 {
490 $test_result_data = $this->object->getTestResult($active_id, $pass);
491 $man_scoring_question_gui_list = [];
492
493 foreach ($test_result_data as $question_data) {
494 if (!isset($question_data['qid'])) {
495 continue;
496 }
497
498 if (!isset($question_data['type'])) {
499 throw new ilTestException('no question type given!');
500 }
501
502 $man_scoring_question_gui_list[ $question_data['qid'] ] = $this->object
503 ->createQuestionGUI('', $question_data['qid']);
504 }
505 return $man_scoring_question_gui_list;
506 }
507
509 {
511 $this,
512 $this->object->getAnonOnlyParticipantIds()
513 );
514
515 if ($with_data) {
516 $participant_list = new \ilTestParticipantList($this->object, $this->user, $this->lng, $this->db);
517 $participant_list->initializeFromDbRows(
518 $this->object->getTestParticipantsForManualScoring(
519 $table->getFilterItemByPostVar('participant_status')->getValue()
520 )
521 );
522
523 $table->setData(
524 $participant_list->getAccessFilteredList(
525 $this->participant_access_filter->getScoreParticipantsUserFilter($this->ref_id)
526 )->getScoringTableRows()
527 );
528 }
529
530 return $table;
531 }
532
533 public function getUserNamePresentation(
534 int $active_id,
535 int|string $pass,
536 ?string $name = null
537 ): string {
538 $anon_only_usr_ids = $this->object->getAnonOnlyParticipantIds();
539 $user_id = $this->object->_getUserIdFromActiveId($active_id);
540 if ($name !== null && !in_array($user_id, $anon_only_usr_ids)) {
541 return $name;
542 }
543
544 return $this->getUserExamId($active_id, (string) $pass);
545 }
546
547 public function getUserExamId(int $active_id, string $pass): string
548 {
549 return \ilObjTest::buildExamId($active_id, $pass, $this->object->getId());
550 }
551}
$check
Definition: buildRTE.php:81
buildManScoringParticipantForm(array $question_gui_list, int $active_id, int $pass, bool $initValues=false)
getUserNamePresentation(int $active_id, int|string $pass, ?string $name=null)
static _getReachedPoints(int $active_id, int $question_id, int $pass)
Class ilInfoScreenGUI.
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.
Class ilObjTestGUI.
const SCORE_LAST_PASS
static getSingleManualFeedback(int $active_id, int $question_id, int $pass)
static _lookupName(int $a_user_id)
This class represents a property form user interface.
static _getUsedHTMLTagsAsString(string $module='')
Base Exception for all Exceptions relating to Modules/Test.
Service GUI class for tests.
ilTestParticipantData $participantData
static setManScoringDone(int $activeId, bool $manScoringDone)
static isManScoringDone(int $active_id)
static stripSlashes(string $a_str, bool $a_strip_html=true, string $a_allow="")
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc