ILIAS  trunk Revision v11.0_alpha-1723-g8e69f309bab
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
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  $pass = $this->fetchPassParameter($active_id);
213 
214  $question_gui_list = $this->getManScoringQuestionGuiList($active_id, $pass);
215  $form = $this->buildManScoringParticipantForm($question_gui_list, $active_id, $pass, false);
216 
217  $form->setValuesByPost();
218 
219  if (!$form->checkInput()) {
220  $this->tpl->setOnScreenMessage('failure', sprintf($this->lng->txt('tst_save_manscoring_failed'), $pass + 1));
221  $this->showManScoringParticipantScreen($form);
222  return false;
223  }
224 
225  $maxPointsByQuestionId = [];
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  $maxPointsByQuestionId[$question_id] = $max_points;
241  }
242 
243  if ($max_points_exceeded) {
244  $this->tpl->setOnScreenMessage('failure', sprintf($this->lng->txt('tst_save_manscoring_failed'), $pass + 1));
245  $this->showManScoringParticipantScreen($form);
246  return false;
247  }
248 
249  foreach (array_keys($question_gui_list) as $question_id) {
250  $reached_points = $this->refinery->kindlyTo()->float()->transform(
251  $form->getItemByPostVar("question__{$question_id}__points")->getValue()
252  );
253 
254  $finalized = (bool) $form->getItemByPostVar("{$question_id}__evaluated")->getchecked();
255 
256  // fix #35543: save manual points only if they differ from the existing points
257  // this prevents a question being set to "answered" if only feedback is entered
258  $old_points = \assQuestion::_getReachedPoints($active_id, $question_id, $pass);
259  if ($reached_points != $old_points) {
261  $active_id,
262  $question_id,
263  $reached_points,
264  $maxPointsByQuestionId[$question_id],
265  $pass,
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  $pass,
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  $notificationData[$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' => $pass + 1,
323  'questions_gui_list' => $question_gui_list,
324  'questions_scoring_data' => $notificationData
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, $pass);
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'), $pass + 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  foreach ($question_gui_list as $question_id => $question_gui) {
394  $question_header = sprintf(
395  $this->lng->txt('tst_manscoring_question_section_header'),
396  $question_gui->getObject()->getTitleForHTMLOutput()
397  );
398  $question_solution = $question_gui->getSolutionOutput($active_id, $pass, false, false, true, false, false, true);
399  $best_solution = $question_gui->getObject()->getSuggestedSolutionOutput();
400 
401  $feedback = \ilObjTest::getSingleManualFeedback($active_id, $question_id, $pass);
402 
403  $disabled = false;
404  if (isset($feedback['finalized_evaluation']) && $feedback['finalized_evaluation'] == 1) {
405  $disabled = true;
406  }
407 
408  $sect = new \ilFormSectionHeaderGUI();
409  $sect->setTitle($question_header . ' [' . $this->lng->txt('question_id_short') . ': ' . $question_gui->getObject()->getId() . ']');
410  $form->addItem($sect);
411 
412  $cust = new \ilCustomInputGUI($this->lng->txt('tst_manscoring_input_question_and_user_solution'));
413  $cust->setHtml($question_solution);
414  $form->addItem($cust);
415 
416  $text = new \ilTextInputGUI($this->lng->txt('tst_change_points_for_question'), "question__{$question_id}__points");
417  if ($initValues) {
418  $text->setValue((string) \assQuestion::_getReachedPoints($active_id, $question_id, $pass));
419  }
420  if ($disabled) {
421  $text->setDisabled($disabled);
422  }
423  $form->addItem($text);
424 
425  $nonedit = new \ilNonEditableValueGUI($this->lng->txt('tst_manscoring_input_max_points_for_question'), "question__{$question_id}__maxpoints");
426  if ($initValues) {
427  $nonedit->setValue($this->questionrepository->getForQuestionId($question_id)->getAvailablePoints());
428  }
429  $form->addItem($nonedit);
430 
431  $area = new \ilTextAreaInputGUI($this->lng->txt('set_manual_feedback'), "question__{$question_id}__feedback");
432  $area->setUseRTE(true);
433  if ($initValues) {
434  $area->setValue(\ilObjTest::getSingleManualFeedback((int) $active_id, (int) $question_id, (int) $pass)['feedback'] ?? '');
435  }
436  if ($disabled) {
437  $area->setDisabled($disabled);
438  }
439  $form->addItem($area);
440 
441  $check = new \ilCheckboxInputGUI($this->lng->txt('finalized_evaluation'), "{$question_id}__evaluated");
442  if ($disabled) {
443  $check->setChecked(true);
444  }
445  $form->addItem($check);
446 
447  if (strlen(trim($best_solution))) {
448  $cust = new \ilCustomInputGUI($this->lng->txt('tst_show_solution_suggested'));
449  $cust->setHtml($best_solution);
450  $form->addItem($cust);
451  }
452  }
453 
454  $sect = new \ilFormSectionHeaderGUI();
455  $sect->setTitle($this->lng->txt('tst_participant'));
456  $form->addItem($sect);
457 
458  $check = new \ilCheckboxInputGUI($this->lng->txt('set_manscoring_done'), 'manscoring_done');
459  if ($initValues && \ilTestService::isManScoringDone($active_id)) {
460  $check->setChecked(true);
461  }
462  $form->addItem($check);
463 
464  $check = new \ilCheckboxInputGUI($this->lng->txt('tst_manscoring_user_notification'), 'manscoring_notify');
465  $form->addItem($check);
466 
467  $form->addCommandButton('saveManScoringParticipantScreen', $this->lng->txt('save'));
468  $form->addCommandButton('saveReturnManScoringParticipantScreen', $this->lng->txt('save_return'));
469  $form->addCommandButton('saveNextManScoringParticipantScreen', $this->lng->txt('save_and_next'));
470 
471  return $form;
472  }
473 
474  private function getManScoringQuestionGuiList(int $active_id, int $pass): array
475  {
476  $test_result_data = $this->object->getTestResult($active_id, $pass);
477 
478  $man_scoring_question_gui_list = [];
479 
480  foreach ($test_result_data as $question_data) {
481  if (!isset($question_data['qid'])) {
482  continue;
483  }
484 
485  if (!isset($question_data['type'])) {
486  throw new ilTestException('no question type given!');
487  }
488 
489  $man_scoring_question_gui_list[ $question_data['qid'] ] = $this->object
490  ->createQuestionGUI('', $question_data['qid']);
491  }
492 
493  return $man_scoring_question_gui_list;
494  }
495 
496  private function buildManScoringParticipantsTable(bool $with_data = false): TestScoringByParticipantTableGUI
497  {
498  $table = new TestScoringByParticipantTableGUI($this);
499 
500  if ($with_data) {
501  $participant_list = new \ilTestParticipantList($this->object, $this->user, $this->lng, $this->db);
502  $participant_list->initializeFromDbRows(
503  $this->object->getTestParticipantsForManualScoring(
504  $table->getFilterItemByPostVar('participant_status')->getValue()
505  )
506  );
507 
508  $table->setData(
509  $participant_list->getAccessFilteredList(
510  $this->participant_access_filter->getScoreParticipantsUserFilter($this->ref_id)
511  )->getScoringTableRows()
512  );
513  }
514 
515  return $table;
516  }
517 }
static _setReachedPoints(int $active_id, int $question_id, float $points, float $maxpoints, int $pass, bool $manualscoring)
Sets the points, a learner has reached answering the question Additionally objective results are upda...
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.
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return 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)
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)