19declare(strict_types=1);
37use Psr\Http\Message\ServerRequestInterface;
41 private const ID =
'pt';
46 private readonly UIFactory $ui_factory,
53 private readonly ResultsDataFactory $results_data_factory,
54 private readonly ResultsPresentationSettings $results_presentation_settings,
55 private readonly \
ilObjUser $current_user,
59 $this->scoring_enabled = $this->test_object->getGlobalSettings()->isManualScoringEnabled();
74 $this->test_request->getRequest(),
75 $this->ui_service->filter()->getData($filter)
80 $table->withActions($this->table_actions->getEnabledActions(...$this->acquireParameters($url_builder)))
86 return $this->
repository->countParticipants($this->test_object->getTestId(), $filter_data);
91 array $visible_column_ids,
95 ?array $additional_parameters
97 $processing_time = $this->test_object->getProcessingTimeInSeconds();
98 $reset_time_on_new_attempt = $this->test_object->getResetProcessingTime();
100 $current_user_timezone = new \DateTimeZone($this->current_user->getTimeZone());
104 $total_duration = $record->getTotalDuration($processing_time);
105 $status_of_attempt = $record->getAttemptOverviewInformation()?->getStatusOfAttempt() ?? StatusOfAttempt::NOT_YET_STARTED;
107 'name' => $this->test_object->buildName($record->getUserId(), $record->getFirstname(), $record->getLastname()),
108 'login' => $record->getLogin(),
109 'matriculation' => $record->getMatriculation(),
110 'total_time_on_task' => $record->getAttemptOverviewInformation()?->getHumanReadableTotalTimeOnTask() ??
'',
111 'status_of_attempt' => $this->
lng->txt($status_of_attempt->value),
112 'id_of_attempt' => $record->getAttemptOverviewInformation()?->getExamId(),
113 'ip_range' => $record->getClientIpTo() !==
'' || $record->getClientIpFrom() !==
''
114 ? sprintf(
'%s - %s', $record->getClientIpFrom(), $record->getClientIpTo())
116 'total_attempts' => $record->getAttemptOverviewInformation()?->getNrOfAttempts() ?? 0,
117 'extra_time' => $record->getExtraTime() > 0 ? sprintf(
'%d min', $record->getExtraTime()) :
'',
118 'total_duration' => $total_duration > 0 ? sprintf(
'%d min', $total_duration / 60) :
'',
119 'remaining_duration' => sprintf(
'%d min', $record->getRemainingDuration($processing_time, $reset_time_on_new_attempt) / 60),
122 if ($this->scoring_enabled) {
123 $row[
'scoring_finalized'] = $record->isScoringFinalized();
126 $first_access = $record->getAttemptOverviewInformation()?->getStartedDate();
127 if ($first_access !==
null) {
128 $row[
'attempt_started_at'] = $first_access->setTimezone($current_user_timezone);
131 $last_access = $record->getLastAccess();
132 if ($last_access !==
null) {
133 $row[
'last_access'] = $last_access->setTimezone($current_user_timezone);
135 if ($record->getActiveId() !==
null
136 && $this->test_access->checkResultsAccessForActiveId(
137 $record->getActiveId(),
138 $this->test_object->getTestId()
139 ) || $record ===
null && $this->test_access->checkParticipantsResultsAccess()) {
140 $row[
'reached_points'] = sprintf(
141 $this->
lng->txt(
'tst_reached_points_of_max'),
142 $record->getAttemptOverviewInformation()?->getReachedPoints(),
143 $record->getAttemptOverviewInformation()?->getAvailablePoints()
145 $row[
'nr_of_answered_questions'] = sprintf(
146 $this->
lng->txt(
'tst_answered_questions_of_total'),
147 $record->getAttemptOverviewInformation()?->getNrOfAnsweredQuestions(),
148 $record->getAttemptOverviewInformation()?->getNrOfTotalQuestions()
150 $row[
'percent_of_available_points'] = $record->getAttemptOverviewInformation()?->getReachedPointsInPercent();
153 if ($status_of_attempt->isFinished()) {
154 $row[
'test_passed'] = $record->getAttemptOverviewInformation()?->hasPassingMark() ??
false;
155 $row[
'mark'] = $record->getAttemptOverviewInformation()?->getMark();
158 yield $this->table_actions->onDataRow(
159 $row_builder->
buildDataRow((
string) $record->getUserId(), $row),
167 return $url_builder->acquireParameters(
181 'solution' => fn(
string $value,
Participant $record) =>
183 'status_of_attempt' => fn(
string $value,
Participant $record) =>
186 'test_passed' => fn(
string $value,
Participant $record) => $value ===
'true'
189 'scoring_finalized' => fn(
string $value,
Participant $record) => $value ===
'true'
200 $processing_time = $this->test_object->getProcessingTimeInSeconds();
201 $reset_time_on_new_attempt = $this->test_object->getResetProcessingTime();
205 'total_duration' =>
static fn(
208 ) =>
$a->getTotalDuration($processing_time) <=>
$b->getTotalDuration($processing_time),
209 'remaining_duration' =>
static fn(
212 ) =>
$a->getRemainingDuration($processing_time, $reset_time_on_new_attempt)
213 <=>
$b->getRemainingDuration($processing_time, $reset_time_on_new_attempt),
215 'status_of_attempt' =>
static fn(
218 ) =>
$a->getAttemptOverviewInformation()?->getStatusOfAttempt()
219 <=>
$b->getAttemptOverviewInformation()?->getStatusOfAttempt(),
220 'reached_points' =>
static fn(
223 ) =>
$a->getAttemptOverviewInformation()?->getReachedPoints()
224 <=>
$b->getAttemptOverviewInformation()?->getReachedPoints(),
225 'nr_of_answered_questions' =>
static fn(
228 ) =>
$a->getAttemptOverviewInformation()?->getNrOfAnsweredQuestions()
229 <=>
$b->getAttemptOverviewInformation()?->getNrOfAnsweredQuestions(),
230 'percent_of_available_points' =>
static fn(
233 ) =>
$a->getAttemptOverviewInformation()?->getReachedPointsInPercent()
234 <=>
$b->getAttemptOverviewInformation()?->getReachedPointsInPercent(),
235 'test_passed' =>
static fn(
238 ) =>
$a->getAttemptOverviewInformation()?->hasPassingMark()
239 <=>
$b->getAttemptOverviewInformation()?->hasPassingMark(),
243 ) =>
$a->getAttemptOverviewInformation()?->getMark() <=>
$b->getAttemptOverviewInformation()?->getMark(),
244 'matriculation' =>
static fn(
247 ) =>
$a->getMatriculation() <=>
$b->getMatriculation(),
248 'id_of_attempt' =>
static fn(
251 ) =>
$a->getAttemptOverviewInformation()?->getExamId() <=>
$b->getAttemptOverviewInformation()?->getExamId(),
252 'total_time_on_task' =>
static fn(
255 ) =>
$a->getAttemptOverviewInformation()?->getTotalTimeOnTask() <=>
$b->getAttemptOverviewInformation()?->getTotalTimeOnTask()
259 private function getFilterComponent(
string $action, ServerRequestInterface $request): FilterComponent
262 $is_input_initially_rendered = [];
263 $field_factory = $this->ui_factory->input()->field();
265 foreach ($this->getFilterFields($field_factory) as $filter_id => $filter) {
266 [$filter_inputs[$filter_id], $is_input_initially_rendered[$filter_id]] = $filter;
269 return $this->ui_service->filter()->standard(
270 'participant_filter',
273 $is_input_initially_rendered,
286 $yes_no_all_options = [
287 'true' => $this->
lng->txt(
'yes'),
288 'false' => $this->
lng->txt(
'no')
291 $solution_options = [
292 'false' => $this->
lng->txt(
'without_solution'),
293 'true' => $this->
lng->txt(
'with_solution')
296 $status_of_attempt_options = [
297 StatusOfAttempt::NOT_YET_STARTED->value => $this->
lng->txt(StatusOfAttempt::NOT_YET_STARTED->value),
298 StatusOfAttempt::RUNNING->value => $this->
lng->txt(StatusOfAttempt::RUNNING->value),
299 StatusOfAttempt::FINISHED_BY_UNKNOWN->value => $this->
lng->txt(StatusOfAttempt::FINISHED_BY_UNKNOWN->value),
300 StatusOfAttempt::FINISHED_BY_ADMINISTRATOR->value => $this->
lng->txt(StatusOfAttempt::FINISHED_BY_ADMINISTRATOR->value),
301 StatusOfAttempt::FINISHED_BY_CRONJOB->value => $this->
lng->txt(StatusOfAttempt::FINISHED_BY_CRONJOB->value),
302 StatusOfAttempt::FINISHED_BY_DURATION->value => $this->
lng->txt(StatusOfAttempt::FINISHED_BY_DURATION->value),
303 StatusOfAttempt::FINISHED_BY_PARTICIPANT->value => $this->
lng->txt(StatusOfAttempt::FINISHED_BY_PARTICIPANT->value),
307 'name' => [$field_factory->text($this->
lng->txt(
'name')),
true],
308 'login' => [$field_factory->text($this->
lng->txt(
'login')),
true],
309 'ip_range' => [$field_factory->text($this->
lng->txt(
'client_ip_range')),
true],
310 'solution' => [$field_factory->select($this->
lng->txt(
'solutions'), $solution_options),
true],
313 if ($this->test_object->getEnableProcessingTime()) {
314 $filters[
'extra_time'] = [$field_factory->select($this->
lng->txt(
'extratime'), $yes_no_all_options),
true];
317 $filters[
'status_of_attempt'] = [
318 $field_factory->select($this->
lng->txt(
'status_of_attempt'), $status_of_attempt_options),
322 $filters[
'test_passed'] = [
323 $field_factory->select($this->
lng->txt(
'tst_passed'), $yes_no_all_options),
327 if ($this->scoring_enabled) {
328 $filters[
'scoring_finalized'] = [
329 $field_factory->select($this->
lng->txt(
'finalized_evaluation'), $yes_no_all_options),
339 return $this->ui_factory
343 $this->
lng->txt(
'list_of_participants'),
347 ->withRequest($request)
348 ->withFilter($filter);
356 $column_factory = $this->ui_factory->table()->column();
359 'name' => $column_factory->text($this->
lng->txt(
'name'))
360 ->withIsSortable(!$this->test_object->getAnonymity())
362 if (!$this->test_object->getAnonymity()) {
363 $columns[
'login'] = $column_factory->text($this->
lng->txt(
'login'))->withIsSortable(
true);
367 'matriculation' => $column_factory->text($this->
lng->txt(
'matriculation'))
368 ->withIsOptional(
true,
false)
369 ->withIsSortable(
true),
370 'ip_range' => $column_factory->text($this->
lng->txt(
'client_ip_range'))
371 ->withIsOptional(
true,
false)
372 ->withIsSortable(
true),
373 'attempt_started_at' => $column_factory->date(
374 $this->
lng->txt(
'tst_attempt_started'),
375 $this->current_user->getDateTimeFormat()
376 )->withIsSortable(
true),
377 'total_time_on_task' => $column_factory->text($this->
lng->txt(
'working_time'))
378 ->withIsOptional(
true,
false),
379 'total_attempts' => $column_factory->number($this->
lng->txt(
'total_attempts'))
380 ->withIsOptional(
true,
false)
381 ->withIsSortable(
true),
384 $columns[
'status_of_attempt'] = $column_factory->text($this->
lng->txt(
'status_of_attempt'))
385 ->withIsSortable(
true);
387 if ($this->test_object->getEnableProcessingTime()) {
388 $columns[
'remaining_duration'] = $column_factory->text($this->
lng->txt(
'remaining_duration'))
389 ->withIsOptional(
true);
390 $columns[
'total_duration'] = $column_factory->text($this->
lng->txt(
'total_duration'))
391 ->withIsOptional(
true,
false);
392 $columns[
'extra_time'] = $column_factory->text($this->
lng->txt(
'extratime'))
393 ->withIsOptional(
true,
false);
396 if ($this->test_object->getMainSettings()->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled()) {
397 $columns[
'id_of_attempt'] = $column_factory->text($this->
lng->txt(
'exam_id_of_attempt'))
398 ->withIsOptional(
true,
false)
399 ->withIsSortable(
true);
402 if ($this->test_access->checkParticipantsResultsAccess()) {
403 $columns[
'reached_points'] = $column_factory->text($this->
lng->txt(
'tst_reached_points'))
404 ->withIsSortable(
true);
405 $columns[
'nr_of_answered_questions'] = $column_factory->text($this->
lng->txt(
'tst_answered_questions'))
406 ->withIsOptional(
true,
false)
407 ->withIsSortable(
true);
408 $columns[
'percent_of_available_points'] = $column_factory->number($this->
lng->txt(
'tst_percent_solved'))
410 ->withIsOptional(
true,
false)
411 ->withIsSortable(
true);
412 $columns[
'test_passed'] = $column_factory->boolean(
413 $this->
lng->txt(
'tst_passed'),
414 $this->ui_factory->symbol()->icon()->custom(
415 'assets/images/standard/icon_checked.svg',
416 $this->lng->txt(
'yes'),
419 $this->ui_factory->symbol()->icon()->custom(
420 'assets/images/standard/icon_unchecked.svg',
421 $this->lng->txt(
'no'),
424 )->withIsSortable(
true)
425 ->withOrderingLabels(
426 "{$this->lng->txt('tst_passed')}, {$this->lng->txt('yes')} {$this->lng->txt('order_option_first')}",
427 "{$this->lng->txt('tst_passed')}, {$this->lng->txt('no')} {$this->lng->txt('order_option_first')}"
429 $columns[
'mark'] = $column_factory->text($this->
lng->txt(
'tst_mark'))
430 ->withIsOptional(
true,
false)
431 ->withIsSortable(
true);
433 if ($this->scoring_enabled) {
434 $columns[
'scoring_finalized'] = $column_factory->boolean(
435 $this->
lng->txt(
'finalized_evaluation'),
436 $this->ui_factory->symbol()->icon()->custom(
437 'assets/images/standard/icon_checked.svg',
438 $this->lng->txt(
'yes'),
441 $this->ui_factory->symbol()->icon()->custom(
442 'assets/images/standard/icon_unchecked.svg',
443 $this->lng->txt(
'no'),
446 )->withIsOptional(
true,
false)
447 ->withIsSortable(
true);
450 $columns[
'last_access'] = $column_factory->date(
451 $this->
lng->txt(
'last_access'),
452 $this->current_user->getDateTimeFormat()
460 if ($this->records !==
null) {
461 return $this->records;
464 $records = iterator_to_array(
466 $this->test_object->getTestId(),
473 $this->records = array_filter(
477 $this->buildAccessFilteredParticipantsList($records)
481 return $this->records;
491 $manage_access_filter = $this->participant_access_filter
492 ->getManageParticipantsUserFilter($this->test_object->getRefId());
493 $access_results_access_filter = $this->participant_access_filter
494 ->getAccessResultsUserFilter($this->test_object->getRefId());
495 $participant_ids = array_map(
499 return $manage_access_filter($participant_ids) + $access_results_access_filter($participant_ids);
508 return $this->limitRecords(
510 $this->filterRecords(
511 $this->results_data_factory->addAttemptOverviewInformationToParticipants(
512 $this->results_presentation_settings,
514 $this->loadRecords($filter_data, $order)
524 private function filterRecords(iterable $records, ?array $filter_data): iterable
526 foreach ($records as $record) {
527 if ($this->matchFilter($record, $filter_data)) {
535 if ($filter ===
null) {
539 $post_load_filters = $this->getPostLoadFilters();
542 foreach ($filter as $key => $value) {
547 $post_load_filter = $post_load_filters[$key] ?? fn() =>
true;
548 $allow = $allow && $post_load_filter($value, $record);
556 $post_load_order_fields = $this->getPostLoadOrderFields();
557 $records = iterator_to_array($records);
560 foreach ($order->
get() as $subject => $direction) {
561 $post_load_order_field = $post_load_order_fields[$subject] ?? fn() => 0;
562 $index = $post_load_order_field($a, $b);
565 return $direction ===
'DESC' ? $index * -1 : $index;
Builds a Color from either hex- or rgb values.
Both the subject and the direction need to be specified when expressing an order.
A simple class to express a naive range of whole positive numbers.
const ACTION_TYPE_PARAMETER
buildAccessFilteredParticipantsList(array $records)
getFilterFields(FieldFactory $field_factory)
getComponents(URLBuilder $url_builder, string $filter_url)
filterRecords(iterable $records, ?array $filter_data)
getFilterComponent(string $action, ServerRequestInterface $request)
orderRecords(iterable $records, Order $order)
getViewControlledRecords(?array $filter_data, Range $range, Order $order)
getTotalRowCount(?array $filter_data, ?array $additional_parameters)
Mainly for the purpose of pagination-support, it is important to know about the total number of recor...
__construct(private readonly UIFactory $ui_factory, private readonly \ilUIService $ui_service, private readonly Language $lng, private readonly \ilTestAccess $test_access, private readonly RequestDataCollector $test_request, private readonly \ilTestParticipantAccessFilterFactory $participant_access_filter, private readonly ParticipantRepository $repository, private readonly ResultsDataFactory $results_data_factory, private readonly ResultsPresentationSettings $results_presentation_settings, private readonly \ilObjUser $current_user, private readonly \ilObjTest $test_object, private readonly ParticipantTableActions $table_actions)
loadRecords(?array $filter, Order $order)
matchFilter(Participant $record, ?array $filter)
execute(URLBuilder $url_builder)
limitRecords(array $records, Range $range)
acquireParameters($url_builder)
getTableComponent(ServerRequestInterface $request, ?array $filter)
hasAnsweredQuestionsForScoredAttempt()
getAttemptOverviewInformation()
This describes commonalities between the different modals.
getRows(DataRowBuilder $row_builder, array $visible_column_ids, Range $range, Order $order, ?array $filter_data, ?array $additional_parameters)
This is called by the table to retrieve rows; map data-records to rows using the $row_builder e....
buildDataRow(string $id, array $record)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples