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