ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
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  && !$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));
228  $this->showManScoringParticipantScreen($form);
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));
252  $this->showManScoringParticipantScreen($form);
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 
380  private function saveReturnManScoringParticipantScreen(): void
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
392  ): \ilPropertyFormGUI {
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 
508  private function buildManScoringParticipantsTable(bool $with_data = false): TestScoringByParticipantTableGUI
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 }
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.
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
getUserNamePresentation(int $active_id, int|string $pass, ?string $name=null)
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)
const SCORE_LAST_PASS
ilTestParticipantData $participantData
__construct(Container $dic, ilPlugin $plugin)
static _getUsedHTMLTagsAsString(string $module='')
static _getParticipantId(int $active_id)
Get user id for active id.
$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)