Passed
Pull Request — master (#7150)
by
unknown
08:54
created

TrackingCourseLog::actionsLeft()   B

Complexity

Conditions 8

Size

Total Lines 113
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 68
nop 3
dl 0
loc 113
rs 7.4537
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\ExtraField as EntityExtraField;
6
use Chamilo\CoreBundle\Enums\ToolIcon;
7
use ChamiloSession as Session;
8
9
class TrackingCourseLog
10
{
11
    /**
12
     * Counts course resources using resource_link / resource_node.
13
     */
14
    public static function countItemResources(): mixed
15
    {
16
        $sessionId = api_get_session_id();
17
        $courseId  = api_get_course_int_id();
18
19
        $tableUser         = Database::get_main_table(TABLE_MAIN_USER);
20
        $tableSession      = Database::get_main_table(TABLE_MAIN_SESSION);
21
        $tableResourceLink = Database::get_main_table('resource_link');
22
        $tableResourceNode = Database::get_main_table('resource_node');
23
        $tableResourceType = Database::get_main_table('resource_type');
24
25
        // Resource types we want to see in this report.
26
        $allowedTypes = [
27
            'files',
28
            'lps',
29
            'exercises',
30
            'glossaries',
31
            'links',
32
            'course_descriptions',
33
            'announcements',
34
            'thematics',
35
            'thematic_advance',
36
            'thematic_plan',
37
        ];
38
        $typesList = "'" . implode("','", $allowedTypes) . "'";
39
40
        $sql = "SELECT COUNT(*) AS total_number_of_items
41
            FROM $tableResourceLink rl
42
            INNER JOIN $tableResourceNode rn ON rn.id = rl.resource_node_id
43
            INNER JOIN $tableResourceType rt ON rt.id = rn.resource_type_id
44
            LEFT JOIN $tableUser u ON u.id = rn.creator_id
45
            LEFT JOIN $tableSession s ON s.id = rl.session_id
46
            WHERE rl.c_id = $courseId";
47
48
        if (empty($sessionId)) {
49
            $sql .= ' AND rl.session_id IS NULL';
50
        } else {
51
            $sessionId = (int) $sessionId;
52
            $sql .= " AND rl.session_id = $sessionId";
53
        }
54
55
        $sql .= " AND rt.title IN ($typesList)";
56
57
        if (!empty($_GET['keyword'])) {
58
            $keyword = Database::escape_string(trim((string) $_GET['keyword']));
59
            $sql .= " AND (
60
            u.username LIKE '%$keyword%' OR
61
            rn.title   LIKE '%$keyword%' OR
62
            rt.title   LIKE '%$keyword%'
63
        )";
64
        }
65
66
        $res = Database::query($sql);
67
        $obj = Database::fetch_object($res);
68
69
        return $obj ? (int) $obj->total_number_of_items : 0;
70
    }
71
72
    /**
73
     * Retrieves resource log data using resource_link/resource_node.
74
     */
75
    public static function getItemResourcesData($from, $numberOfItems, $column, $direction): array
76
    {
77
        $sessionId = api_get_session_id();
78
        $courseId  = api_get_course_int_id();
79
80
        $tableUser         = Database::get_main_table(TABLE_MAIN_USER);
81
        $tableSession      = Database::get_main_table(TABLE_MAIN_SESSION);
82
        $tableSessionUser  = Database::get_main_table(TABLE_MAIN_SESSION_USER);
83
        $tableResourceLink = Database::get_main_table('resource_link');
84
        $tableResourceNode = Database::get_main_table('resource_node');
85
        $tableResourceType = Database::get_main_table('resource_type');
86
87
        $column    = (int) $column;
88
        $direction = strtolower(trim((string) $direction));
89
        $direction = in_array($direction, ['asc', 'desc'], true) ? $direction : 'asc';
90
91
        // Resource types we want to see in this report.
92
        $allowedTypes = [
93
            'files',
94
            'lps',
95
            'exercises',
96
            'glossaries',
97
            'links',
98
            'course_descriptions',
99
            'announcements',
100
            'thematics',
101
            'thematic_advance',
102
            'thematic_plan',
103
        ];
104
        $typesList = "'" . implode("','", $allowedTypes) . "'";
105
106
        $sql = "SELECT
107
                rl.id            AS ref,
108
                rl.created_at    AS col6,
109
                rl.visibility    AS col7,
110
                rn.title         AS document_title,
111
                rt.title         AS resource_type_title,
112
                creator.id       AS user_id,
113
                creator.username AS col3,
114
                s.title           AS session_name,
115
                coach.username   AS coach_username
116
            FROM $tableResourceLink rl
117
            INNER JOIN $tableResourceNode rn ON rn.id = rl.resource_node_id
118
            INNER JOIN $tableResourceType rt ON rt.id = rn.resource_type_id
119
            LEFT JOIN $tableUser creator ON creator.id = rn.creator_id
120
            LEFT JOIN $tableSession s ON s.id = rl.session_id
121
            LEFT JOIN $tableSessionUser su ON su.session_id = rl.session_id
122
            LEFT JOIN $tableUser coach ON coach.id = su.user_id
123
            WHERE rl.c_id = $courseId";
124
125
        if (empty($sessionId)) {
126
            $sql .= ' AND rl.session_id IS NULL';
127
        } else {
128
            $sessionId = (int) $sessionId;
129
            $sql .= " AND rl.session_id = $sessionId";
130
        }
131
132
        $sql .= " AND rt.title IN ($typesList)";
133
134
        if (!empty($_GET['keyword'])) {
135
            $keyword = Database::escape_string(trim((string) $_GET['keyword']));
136
            $sql .= " AND (
137
                creator.username LIKE '%$keyword%' OR
138
                rn.title         LIKE '%$keyword%' OR
139
                rt.title         LIKE '%$keyword%'
140
            )";
141
        }
142
143
        // Decide ORDER BY based on requested column.
144
        switch ($column) {
145
            case 0: // Tool
146
                $orderBy = 'rt.title';
147
                break;
148
            case 2: // Session
149
                $orderBy = 's.title';
150
                break;
151
            case 3: // Username
152
                $orderBy = 'creator.username';
153
                break;
154
            case 5: // Document
155
                $orderBy = 'rn.title';
156
                break;
157
            case 6: // Date
158
            default:
159
                $orderBy = 'rl.created_at';
160
                break;
161
        }
162
163
        $sql .= " ORDER BY $orderBy $direction";
164
165
        $from = (int) $from;
166
        if ($from) {
167
            $numberOfItems = (int) $numberOfItems;
168
            $sql .= " LIMIT $from, $numberOfItems";
169
        }
170
171
        $res       = Database::query($sql);
172
        $resources = [];
173
174
        while ($row = Database::fetch_array($res)) {
175
            $legacyTool = self::mapResourceTypeTitleToLegacyTool((string) $row['resource_type_title']);
176
177
            if (null === $legacyTool) {
178
                // Ignore resource types that do not map to a legacy tool.
179
                continue;
180
            }
181
182
            // Build a clean row so SortableTable only sees columns 0..6.
183
            $displayRow = [];
184
185
            // Internal legacy columns used by CSV/XLS export.
186
            $displayRow['ref']                 = (int) $row['ref'];
187
            $displayRow['col6']                = $row['col6']; // created_at
188
            $displayRow['col7']                = (int) $row['col7']; // visibility
189
            $displayRow['user_id']             = isset($row['user_id']) ? (int) $row['user_id'] : 0;
190
            $displayRow['col3']                = $row['col3']; // username
191
            $displayRow['document_title']      = $row['document_title'] ?? '';
192
            $displayRow['resource_type_title'] = $row['resource_type_title'];
193
            $displayRow['session_name']        = $row['session_name'] ?? '';
194
            $displayRow['coach_username']      = $row['coach_username'] ?? '';
195
            $displayRow['col0']                = $legacyTool;
196
            $displayRow['col1']                = 'Created';
197
198
            // Column 0: Tool.
199
            $displayRow[0] = get_lang('Tool'.api_ucfirst($legacyTool));
200
201
            // Column 1: Event type.
202
            $displayRow[1] = get_lang($displayRow['col1']);
203
204
            // Column 2: Session + coach.
205
            $sessionText = '';
206
            if (!empty($displayRow['session_name'])) {
207
                $sessionText = $displayRow['session_name'];
208
                if (!empty($displayRow['coach_username'])) {
209
                    $sessionText .= '<br />'.get_lang('Coach').': '.$displayRow['coach_username'];
210
                }
211
            }
212
            $displayRow[2] = $sessionText;
213
214
            // Column 3: Username (linked to profile).
215
            $displayRow[3] = '';
216
            if (!empty($displayRow['col3']) && !empty($displayRow['user_id'])) {
217
                $userInfo          = api_get_user_info($displayRow['user_id']);
218
                $displayRow['col3'] = Display::url($displayRow['col3'], $userInfo['profile_url']);
219
                $displayRow[3]      = $displayRow['col3'];
220
            }
221
222
            // Column 4: IP address.
223
            $ip = '';
224
            if (!empty($displayRow['user_id']) && !empty($displayRow['col6'])) {
225
                $ip = Tracking::get_ip_from_user_event(
226
                    (int) $displayRow['user_id'],
227
                    $displayRow['col6'],
228
                    true
229
                );
230
            }
231
            if (empty($ip)) {
232
                $ip = get_lang('Unknown');
233
            }
234
            $displayRow[4] = $ip;
235
236
            // Column 5: Document title.
237
            $displayRow[5] = $displayRow['document_title'];
238
239
            // Column 6: Date.
240
            $displayRow[6] = api_convert_and_format_date(
241
                $displayRow['col6'],
242
                null,
243
                date_default_timezone_get()
244
            );
245
246
            $resources[] = $displayRow;
247
        }
248
249
        return $resources;
250
    }
251
252
    /**
253
     * Retrieves the name and associated table for a given tool.
254
     */
255
    public static function getToolNameTable(string $tool): array
256
    {
257
        $linkTool = '';
258
        $idTool = '';
259
260
        switch ($tool) {
261
            case 'document':
262
                $tableName = TABLE_DOCUMENT;
263
                $linkTool = 'document/document.php';
264
                $idTool = 'id';
265
                break;
266
            case 'learnpath':
267
                $tableName = TABLE_LP_MAIN;
268
                $linkTool = 'lp/lp_controller.php';
269
                $idTool = 'id';
270
                break;
271
            case 'quiz':
272
                $tableName = TABLE_QUIZ_TEST;
273
                $linkTool = 'exercise/exercise.php';
274
                $idTool = 'iid';
275
                break;
276
            case 'glossary':
277
                $tableName = TABLE_GLOSSARY;
278
                $linkTool = 'glossary/index.php';
279
                $idTool = 'glossary_id';
280
                break;
281
            case 'link':
282
                $tableName = TABLE_LINK;
283
                $linkTool = 'link/link.php';
284
                $idTool = 'id';
285
                break;
286
            case 'course_description':
287
                $tableName = TABLE_COURSE_DESCRIPTION;
288
                $linkTool = 'course_description/';
289
                $idTool = 'id';
290
                break;
291
            case 'announcement':
292
                $tableName = TABLE_ANNOUNCEMENT;
293
                $linkTool = 'announcements/announcements.php';
294
                $idTool = 'id';
295
                break;
296
            case 'thematic':
297
                $tableName = TABLE_THEMATIC;
298
                $linkTool = 'course_progress/index.php';
299
                $idTool = 'id';
300
                break;
301
            case 'thematic_advance':
302
                $tableName = TABLE_THEMATIC_ADVANCE;
303
                $linkTool = 'course_progress/index.php';
304
                $idTool = 'id';
305
                break;
306
            case 'thematic_plan':
307
                $tableName = TABLE_THEMATIC_PLAN;
308
                $linkTool = 'course_progress/index.php';
309
                $idTool = 'id';
310
                break;
311
            default:
312
                $tableName = $tool;
313
                break;
314
        }
315
316
        return [
317
            'table_name' => $tableName,
318
            'link_tool' => $linkTool,
319
            'id_tool' => $idTool,
320
        ];
321
    }
322
323
    /**
324
     * Displays additional profile fields, excluding specific fields if provided.
325
     */
326
    public static function displayAdditionalProfileFields(array $exclude = [], $formAction = null): string
327
    {
328
        $formAction = $formAction ?: 'courseLog.php';
329
330
        // getting all the extra profile fields that are defined by the platform administrator
331
        $extraFields = UserManager::get_extra_fields(0, 50);
332
333
        // creating the form
334
        $return = '<form action="'.$formAction.'" method="get" name="additional_profile_field_form"
335
            id="additional_profile_field_form">';
336
        // the select field with the additional user profile fields, this is where we select the field of which we want to see
337
        // the information the users have entered or selected.
338
        $return .= '<div class="form-group">';
339
        $return .= '<select class="chzn-select" name="additional_profile_field[]" multiple>';
340
        $return .= '<option value="-">'.get_lang('Select user profile field to add').'</option>';
341
        $extraFieldsToShow = 0;
342
        foreach ($extraFields as $field) {
343
            // exclude extra profile fields by id
344
            if (in_array($field[3], $exclude)) {
345
                continue;
346
            }
347
            // show only extra fields that are visible + and can be filtered, added by J.Montoya
348
            if ($field[6] == 1 && $field[8] == 1) {
349
                if (isset($_GET['additional_profile_field']) && in_array($field[0], $_GET['additional_profile_field'])) {
350
                    $selected = 'selected="selected"';
351
                } else {
352
                    $selected = '';
353
                }
354
                $extraFieldsToShow++;
355
                $return .= '<option value="'.$field[0].'" '.$selected.'>'.$field[3].'</option>';
356
            }
357
        }
358
        $return .= '</select>';
359
        $return .= '</div>';
360
361
        // the form elements for the $_GET parameters (because the form is passed through GET
362
        foreach ($_GET as $key => $value) {
363
            if ($key != 'additional_profile_field') {
364
                $return .= '<input type="hidden" name="'.Security::remove_XSS($key).'" value="'.Security::remove_XSS(
365
                        $value
366
                    ).'" />';
367
            }
368
        }
369
        // the submit button
370
        $return .= '<div class="form-group">';
371
        $return .= '<button class="save btn btn-primary" type="submit">'
372
            .get_lang('Add user profile field').'</button>';
373
        $return .= '</div>';
374
        $return .= '</form>';
375
376
        return $extraFieldsToShow > 0 ? $return : '';
377
    }
378
379
    /**
380
     * This function gets all the information of a certrain ($field_id)
381
     * additional profile field for a specific list of users is more efficent
382
     * than get_addtional_profile_information_of_field() function
383
     * It gets the information of all the users so that it can be displayed
384
     * in the sortable table or in the csv or xls export.
385
     *
386
     * @param int $fieldId field id
387
     * @param array $users list of user ids
388
     *
389
     * @author     Julio Montoya <[email protected]>
390
     *
391
     * @since      Nov 2009
392
     *
393
     * @version    1.8.6.2
394
     */
395
    public static function getAdditionalProfileInformationOfFieldByUser($fieldId, $users): array
396
    {
397
        // Database table definition
398
        $tableUser = Database::get_main_table(TABLE_MAIN_USER);
399
        $tableUserFieldValues = Database::get_main_table(TABLE_EXTRA_FIELD_VALUES);
400
        $extraField = Database::get_main_table(TABLE_EXTRA_FIELD);
401
        $resultExtraField = UserManager::get_extra_field_information($fieldId);
402
        $return = [];
403
        if (!empty($users)) {
404
            if ($resultExtraField['field_type'] == UserManager::USER_FIELD_TYPE_TAG) {
405
                foreach ($users as $user_id) {
406
                    $userResult = UserManager::get_user_tags($user_id, $fieldId);
407
                    $tagList = [];
408
                    foreach ($userResult as $item) {
409
                        $tagList[] = $item['tag'];
410
                    }
411
                    $return[$user_id][] = implode(', ', $tagList);
412
                }
413
            } else {
414
                $newUserArray = [];
415
                foreach ($users as $user_id) {
416
                    $newUserArray[] = "'".$user_id."'";
417
                }
418
                $users = implode(',', $newUserArray);
419
                $extraFieldType = EntityExtraField::USER_FIELD_TYPE;
420
                // Selecting only the necessary information NOT ALL the user list
421
                $sql = "SELECT user.user_id, v.value
422
                        FROM $tableUser user
423
                        INNER JOIN $tableUserFieldValues v
424
                        ON (user.user_id = v.item_id)
425
                        INNER JOIN $extraField f
426
                        ON (f.id = v.field_id)
427
                        WHERE
428
                            f.extra_field_type = $extraFieldType AND
429
                            v.field_id=".intval($fieldId)." AND
430
                            user.user_id IN ($users)";
431
432
                $result = Database::query($sql);
433
                while ($row = Database::fetch_array($result)) {
434
                    // get option value for field type double select by id
435
                    if (!empty($row['value'])) {
436
                        if ($resultExtraField['field_type'] ==
437
                            ExtraField::FIELD_TYPE_DOUBLE_SELECT
438
                        ) {
439
                            $idDoubleSelect = explode(';', $row['value']);
440
                            if (is_array($idDoubleSelect)) {
441
                                $value1 = $resultExtraField['options'][$idDoubleSelect[0]]['option_value'];
442
                                $value2 = $resultExtraField['options'][$idDoubleSelect[1]]['option_value'];
443
                                $row['value'] = ($value1.';'.$value2);
444
                            }
445
                        }
446
447
                        if ($resultExtraField['field_type'] == ExtraField::FIELD_TYPE_SELECT_WITH_TEXT_FIELD) {
448
                            $parsedValue = explode('::', $row['value']);
449
450
                            if ($parsedValue) {
451
                                $value1 = $resultExtraField['options'][$parsedValue[0]]['display_text'];
452
                                $value2 = $parsedValue[1];
453
454
                                $row['value'] = "$value1: $value2";
455
                            }
456
                        }
457
458
                        if ($resultExtraField['field_type'] == ExtraField::FIELD_TYPE_TRIPLE_SELECT) {
459
                            [$level1, $level2, $level3] = explode(';', $row['value']);
460
461
                            $row['value'] = $resultExtraField['options'][$level1]['display_text'].' / ';
462
                            $row['value'] .= $resultExtraField['options'][$level2]['display_text'].' / ';
463
                            $row['value'] .= $resultExtraField['options'][$level3]['display_text'];
464
                        }
465
                    }
466
                    // get other value from extra field
467
                    $return[$row['user_id']][] = $row['value'];
468
                }
469
            }
470
        }
471
472
        return $return;
473
    }
474
475
    /**
476
     * Get number of users for sortable with pagination.
477
     */
478
    public static function getNumberOfUsers(array $conditions): int
479
    {
480
        $conditions['get_count'] = true;
481
482
        return self::getUserData(0, 0, 0, '', $conditions);
483
    }
484
485
    /**
486
     * Get data for users list in sortable with pagination.
487
     *
488
     * @param int         $from
489
     * @param int         $numberOfItems
490
     * @param int         $column
491
     * @param string      $direction
492
     * @param array       $conditions
493
     * @param bool        $exerciseToCheckConfig
494
     * @param bool        $displaySessionInfo
495
     * @param string|null $courseCode
496
     * @param int|null    $sessionId
497
     * @param bool        $exportCsv
498
     * @param array       $userIds
499
     *
500
     * @return array
501
     */
502
    public static function getUserData(
503
        $from,
504
        $numberOfItems,
505
        $column,
506
        $direction,
507
        array $conditions = [],
508
        bool $exerciseToCheckConfig = true,
509
        bool $displaySessionInfo = false,
510
        ?string $courseCode = null,
511
        ?int $sessionId = null,
512
        bool $exportCsv = false,
513
        array $userIds = []
514
    ) {
515
        $includeInvitedUsers = $conditions['include_invited_users'] ?? false;
516
        $getCount            = $conditions['get_count'] ?? false;
517
518
        $csvContent     = [];
519
        $tblUser        = Database::get_main_table(TABLE_MAIN_USER);
520
        $tblUrlRelUser  = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
521
        $accessUrlId    = api_get_current_access_url_id();
522
523
        // ---------------------------------------------------------------------
524
        // Resolve course / session context if not explicitly provided
525
        // ---------------------------------------------------------------------
526
        if ($sessionId === null) {
527
            $sessionId = (int) api_get_session_id();
528
        }
529
530
        if (empty($courseCode)) {
531
            $courseInfo = api_get_course_info(); // current course
532
        } else {
533
            $courseInfo = api_get_course_info($courseCode);
534
        }
535
536
        if (empty($courseInfo)) {
537
            // Failsafe: no course context, nothing to show
538
            return [];
539
        }
540
541
        $courseId   = (int) $courseInfo['real_id'];
542
        $courseCode = $courseInfo['code'] ?? $courseCode;
543
544
        // ---------------------------------------------------------------------
545
        // Build user filter (single user vs list of users)
546
        // ---------------------------------------------------------------------
547
        if (!empty($userIds) && is_array($userIds)) {
548
            $userIds      = array_map('intval', $userIds);
549
            $conditionUser = ' WHERE user.id IN ('.implode(',', $userIds).') ';
550
        } else {
551
            $conditionUser = ' WHERE user.id = '.(int) $userIds.' ';
552
        }
553
554
        // Simple keyword filter
555
        if (!empty($_GET['user_keyword'])) {
556
            $keyword       = trim(Database::escape_string($_GET['user_keyword']));
557
            $conditionUser .= " AND (
558
                user.firstname LIKE '%".$keyword."%' OR
559
                user.lastname  LIKE '%".$keyword."%'  OR
560
                user.username  LIKE '%".$keyword."%'  OR
561
                user.email     LIKE '%".$keyword."%'
562
            ) ";
563
        }
564
565
        // Multiple URL restriction
566
        $urlTable     = '';
567
        $urlCondition = '';
568
        if (api_is_multiple_url_enabled()) {
569
            $urlTable     = " INNER JOIN $tblUrlRelUser AS url_users ON (user.id = url_users.user_id)";
570
            $urlCondition = " AND access_url_id = '$accessUrlId'";
571
        }
572
573
        // Exclude invited users if needed
574
        $invitedUsersCondition = '';
575
        if (!$includeInvitedUsers) {
576
            $invitedUsersCondition = ' AND user.status != '.INVITEE;
577
        }
578
579
        // ---------------------------------------------------------------------
580
        // Base SELECT
581
        // ---------------------------------------------------------------------
582
        $select = '
583
            SELECT user.id          AS user_id,
584
                   user.official_code AS col0,
585
                   user.lastname      AS col1,
586
                   user.firstname     AS col2,
587
                   user.username      AS col3,
588
                   user.email         AS col4';
589
590
        if ($getCount) {
591
            $select = 'SELECT COUNT(DISTINCT(user.id)) AS count';
592
        }
593
594
        // Extra joins / where from conditions (classes, extra fields, etc.)
595
        $sqlInjectJoins = '';
596
        $where          = 'AND 1 = 1 ';
597
        $sqlInjectWhere = '';
598
        if (!empty($conditions)) {
599
            if (isset($conditions['inject_joins'])) {
600
                $sqlInjectJoins = $conditions['inject_joins'];
601
            }
602
            if (isset($conditions['where'])) {
603
                $where = $conditions['where'];
604
            }
605
            if (isset($conditions['inject_where'])) {
606
                $sqlInjectWhere = $conditions['inject_where'];
607
            }
608
609
            $injectExtraFields = $conditions['inject_extra_fields'] ?? 1;
610
            $injectExtraFields = rtrim($injectExtraFields, ', ');
611
            if (false === $getCount) {
612
                $select .= " , $injectExtraFields";
613
            }
614
        }
615
616
        $sql = "$select
617
            FROM $tblUser AS user
618
            $urlTable
619
            $sqlInjectJoins
620
            $conditionUser
621
            $urlCondition
622
            $invitedUsersCondition
623
            $where
624
            $sqlInjectWhere
625
        ";
626
627
        // ---------------------------------------------------------------------
628
        // Sorting / limits
629
        // ---------------------------------------------------------------------
630
        $direction = strtoupper($direction);
631
        if (!in_array($direction, ['ASC', 'DESC'], true)) {
632
            $direction = 'ASC';
633
        }
634
635
        $column        = (int) $column;
636
        $from          = (int) $from;
637
        $numberOfItems = (int) $numberOfItems;
638
639
        if ($getCount) {
640
            $res = Database::query($sql);
641
            $row = Database::fetch_array($res);
642
643
            return (int) $row['count'];
644
        }
645
646
        $sortByFirstName = api_sort_by_first_name();
647
        if ($sortByFirstName) {
648
            // Invert columns 1/2 if we sort by firstname
649
            if (1 === $column) {
650
                $column = 2;
651
            } elseif (2 === $column) {
652
                $column = 1;
653
            }
654
        }
655
656
        $sql .= " ORDER BY col$column $direction ";
657
        $sql .= " LIMIT $from, $numberOfItems";
658
659
        $res   = Database::query($sql);
660
        $users = [];
661
662
        // ---------------------------------------------------------------------
663
        // Course data required for progress / scores
664
        // ---------------------------------------------------------------------
665
        $totalSurveys  = 0;
666
        $totalExercises = ExerciseLib::get_all_exercises(
667
            $courseInfo,
668
            $sessionId
669
        );
670
671
        // Preload survey info only when we are outside a session
672
        $surveyUserList = [];
673
        if (empty($sessionId)) {
674
            $courseCodeForSurvey = $courseCode;
675
            $surveyList          = [];
676
677
            if (!empty($courseCodeForSurvey)) {
678
                $surveyList = SurveyManager::get_surveys($courseCodeForSurvey);
679
            }
680
681
            if (!empty($surveyList)) {
682
                $totalSurveys = count($surveyList);
683
684
                foreach ($surveyList as $survey) {
685
                    if (!is_array($survey)) {
686
                        continue;
687
                    }
688
689
                    // Support both "survey_id" and "id"
690
                    $surveyId = $survey['survey_id'] ?? ($survey['id'] ?? null);
691
                    $surveyId = (int) $surveyId;
692
693
                    if ($surveyId <= 0) {
694
                        continue;
695
                    }
696
697
                    $userList = SurveyManager::get_people_who_filled_survey(
698
                        $surveyId,
699
                        false,
700
                        $courseId
701
                    );
702
703
                    foreach ($userList as $userId) {
704
                        if (isset($surveyUserList[$userId])) {
705
                            $surveyUserList[$userId]++;
706
                        } else {
707
                            $surveyUserList[$userId] = 1;
708
                        }
709
                    }
710
                }
711
            }
712
        }
713
714
        $urlBase = api_get_path(WEB_CODE_PATH).'my_space/myStudents.php?details=true'
715
            .'&cid='.$courseId
716
            .'&course='.$courseCode
717
            .'&origin=tracking_course'
718
            .'&sid='.$sessionId;
719
720
        Session::write('user_id_list', []);
721
        $userIdList = [];
722
723
        // Exercises to show as extra columns (best attempt)
724
        $exerciseResultsToCheck = [];
725
        if ($exerciseToCheckConfig) {
726
            $addExerciseOption = api_get_setting('exercise.add_exercise_best_attempt_in_report', true);
727
            if (!empty($addExerciseOption)
728
                && isset($addExerciseOption['courses'], $addExerciseOption['courses'][$courseCode])
729
            ) {
730
                foreach ($addExerciseOption['courses'][$courseCode] as $exerciseId) {
731
                    $exercise = new Exercise();
732
                    $exercise->read($exerciseId);
733
                    if (!empty($exercise->iid)) {
734
                        $exerciseResultsToCheck[] = $exercise;
735
                    }
736
                }
737
            }
738
        }
739
740
        $lpShowMaxProgress = 'true' === api_get_setting('lp.lp_show_max_progress_instead_of_average');
741
        if ('true' === api_get_setting('lp.lp_show_max_progress_or_average_enable_course_level_redefinition')) {
742
            $lpShowProgressCourseSetting = api_get_course_setting(
743
                'lp_show_max_or_average_progress',
744
                $courseInfo,
745
                true
746
            );
747
            if (in_array($lpShowProgressCourseSetting, ['max', 'average'], true)) {
748
                $lpShowMaxProgress = ('max' === $lpShowProgressCourseSetting);
749
            }
750
        }
751
752
        // ---------------------------------------------------------------------
753
        // Main per-user loop
754
        // ---------------------------------------------------------------------
755
        while ($user = Database::fetch_array($res, 'ASSOC')) {
756
            $userIdList[]          = $user['user_id'];
757
            $user['official_code'] = $user['col0'];
758
            $user['username']      = $user['col3'];
759
760
            $user['time'] = api_time_to_hms(
761
                Tracking::get_time_spent_on_the_course(
762
                    $user['user_id'],
763
                    $courseId,
764
                    $sessionId
765
                )
766
            );
767
768
            $avgStudentScore = Tracking::get_avg_student_score(
769
                $user['user_id'],
770
                api_get_course_entity($courseId),
771
                [],
772
                api_get_session_entity($sessionId)
773
            );
774
775
            $averageBestScore = Tracking::get_avg_student_score(
776
                $user['user_id'],
777
                api_get_course_entity($courseId),
778
                [],
779
                api_get_session_entity($sessionId),
780
                false,
781
                false,
782
                true
783
            );
784
785
            $avgStudentProgress = Tracking::get_avg_student_progress(
786
                $user['user_id'],
787
                api_get_course_entity($courseId),
788
                [],
789
                api_get_session_entity($sessionId)
790
            );
791
792
            if (empty($avgStudentProgress)) {
793
                $avgStudentProgress = 0;
794
            }
795
            $user['average_progress'] = $avgStudentProgress.'%';
796
797
            $totalUserExercise = Tracking::get_exercise_student_progress(
798
                $totalExercises,
799
                $user['user_id'],
800
                $courseId,
801
                $sessionId
802
            );
803
            $user['exercise_progress'] = $totalUserExercise;
804
805
            $totalUserExercise = Tracking::get_exercise_student_average_best_attempt(
806
                $totalExercises,
807
                $user['user_id'],
808
                $courseId,
809
                $sessionId
810
            );
811
            $user['exercise_average_best_attempt'] = $totalUserExercise;
812
813
            $user['student_score'] = is_numeric($avgStudentScore)
814
                ? $avgStudentScore.'%'
815
                : $avgStudentScore;
816
817
            $user['student_score_best'] = is_numeric($averageBestScore)
818
                ? $averageBestScore.'%'
819
                : $averageBestScore;
820
821
            // Extra specific exercises as columns
822
            $exerciseResults = [];
823
            if (!empty($exerciseResultsToCheck)) {
824
                foreach ($exerciseResultsToCheck as $exercise) {
825
                    $bestExerciseResult = Event::get_best_attempt_exercise_results_per_user(
826
                        $user['user_id'],
827
                        $exercise->iid,
828
                        $courseId,
829
                        $sessionId,
830
                        false
831
                    );
832
833
                    $best = null;
834
                    if ($bestExerciseResult) {
835
                        $best = $bestExerciseResult['exe_result'] / $bestExerciseResult['exe_weighting'];
836
                        $best = round($best, 2) * 100;
837
                        $best .= '%';
838
                    }
839
                    $exerciseResults['exercise_'.$exercise->iid] = $best;
840
                }
841
            }
842
843
            $user['first_connection'] = Tracking::get_first_connection_date_on_the_course(
844
                $user['user_id'],
845
                $courseId,
846
                $sessionId,
847
                !$exportCsv
848
            );
849
850
            $user['last_connection'] = Tracking::get_last_connection_date_on_the_course(
851
                $user['user_id'],
852
                $courseInfo,
853
                $sessionId,
854
                !$exportCsv
855
            );
856
857
            $user['count_assignments'] = Tracking::countStudentPublications(
858
                $courseId,
859
                $sessionId
860
            );
861
862
            $user['count_messages'] = Tracking::countStudentMessages(
863
                $courseId,
864
                $sessionId
865
            );
866
867
            $user['lp_finalization_date'] = Tracking::getCourseLpFinalizationDate(
868
                $user['user_id'],
869
                $courseId,
870
                $sessionId,
871
                !$exportCsv
872
            );
873
874
            $user['quiz_finalization_date'] = Tracking::getCourseQuizLastFinalizationDate(
875
                $user['user_id'],
876
                $courseId,
877
                $sessionId,
878
                !$exportCsv
879
            );
880
881
            if ($exportCsv) {
882
                $user['first_connection']       = !empty($user['first_connection'])
883
                    ? api_get_local_time($user['first_connection'])
884
                    : '-';
885
                $user['last_connection']        = !empty($user['last_connection'])
886
                    ? api_get_local_time($user['last_connection'])
887
                    : '-';
888
                $user['lp_finalization_date']   = !empty($user['lp_finalization_date'])
889
                    ? api_get_local_time($user['lp_finalization_date'])
890
                    : '-';
891
                $user['quiz_finalization_date'] = !empty($user['quiz_finalization_date'])
892
                    ? api_get_local_time($user['quiz_finalization_date'])
893
                    : '-';
894
            }
895
896
            if (empty($sessionId)) {
897
                $filled = $surveyUserList[$user['user_id']] ?? 0;
898
                $user['survey'] = $totalSurveys > 0
899
                    ? $filled.' / '.$totalSurveys
900
                    : '0 / 0';
901
            }
902
903
            $url        = $urlBase.'&student='.$user['user_id'];
904
            $user['link'] = '<a href="'.$url.'">
905
                    '.Display::return_icon('2rightarrow.png', get_lang('Details')).'
906
                 </a>';
907
908
            // -------------------------------------------------------------
909
            // Build final row
910
            // -------------------------------------------------------------
911
            $userRow = [];
912
            if ($displaySessionInfo && !empty($sessionId)) {
913
                $sessionInfo                   = api_get_session_info($sessionId);
914
                $userRow['session_name']       = $sessionInfo['name'];
915
                $userRow['session_startdate']  = $sessionInfo['access_start_date'];
916
                $userRow['session_enddate']    = $sessionInfo['access_end_date'];
917
                $userRow['course_name']        = $courseInfo['name'];
918
            }
919
920
            $userRow['official_code'] = $user['official_code'];
921
            if ($sortByFirstName) {
922
                $userRow['firstname'] = $user['col2'];
923
                $userRow['lastname']  = $user['col1'];
924
            } else {
925
                $userRow['lastname']  = $user['col1'];
926
                $userRow['firstname'] = $user['col2'];
927
            }
928
            $userRow['username']                     = $user['username'];
929
            $userRow['time']                         = $user['time'];
930
            $userRow['average_progress']             = $user['average_progress'];
931
            $userRow['exercise_progress']            = $user['exercise_progress'];
932
            $userRow['exercise_average_best_attempt']= $user['exercise_average_best_attempt'];
933
            $userRow['student_score']                = $user['student_score'];
934
            $userRow['student_score_best']           = $user['student_score_best'];
935
936
            if (!empty($exerciseResults)) {
937
                foreach ($exerciseResults as $exerciseId => $bestResult) {
938
                    $userRow[$exerciseId] = $bestResult;
939
                }
940
            }
941
942
            $userRow['count_assignments'] = $user['count_assignments'];
943
            $userRow['count_messages']    = $user['count_messages'];
944
945
            $userGroupManager = new UserGroupModel();
946
            if ($exportCsv) {
947
                $userRow['classes'] = implode(
948
                    ',',
949
                    $userGroupManager->getNameListByUser($user['user_id'], UserGroupModel::NORMAL_CLASS)
950
                );
951
            } else {
952
                $userRow['classes'] = $userGroupManager->getLabelsFromNameList(
953
                    $user['user_id'],
954
                    UserGroupModel::NORMAL_CLASS
955
                );
956
            }
957
958
            if (empty($sessionId)) {
959
                $userRow['survey'] = $user['survey'];
960
            } else {
961
                $userSession              = SessionManager::getUserSession($user['user_id'], $sessionId);
962
                $userRow['registered_at'] = '';
963
                if ($userSession) {
964
                    $userRow['registered_at'] = api_get_local_time($userSession['registered_at']);
965
                }
966
            }
967
968
            $userRow['first_connection']      = $user['first_connection'];
969
            $userRow['last_connection']       = $user['last_connection'];
970
            $userRow['lp_finalization_date']  = $user['lp_finalization_date'];
971
            $userRow['quiz_finalization_date']= $user['quiz_finalization_date'];
972
973
            // Extra profile fields selected by the teacher
974
            if (isset($_GET['additional_profile_field'])) {
975
                $data          = Session::read('additional_user_profile_info');
976
                $extraFieldInfo = Session::read('extra_field_info');
977
978
                foreach ($_GET['additional_profile_field'] as $fieldId) {
979
                    if (isset($data[$fieldId]) && isset($data[$fieldId][$user['user_id']])) {
980
                        if (is_array($data[$fieldId][$user['user_id']])) {
981
                            $userRow[$extraFieldInfo[$fieldId]['variable']] = implode(
982
                                ', ',
983
                                $data[$fieldId][$user['user_id']]
984
                            );
985
                        } else {
986
                            $userRow[$extraFieldInfo[$fieldId]['variable']] = $data[$fieldId][$user['user_id']];
987
                        }
988
                    } else {
989
                        $userRow[$extraFieldInfo[$fieldId]['variable']] = '';
990
                    }
991
                }
992
            }
993
994
            $data                  = Session::read('default_additional_user_profile_info');
995
            $defaultExtraFieldInfo = Session::read('default_extra_field_info');
996
            if (!empty($defaultExtraFieldInfo) && !empty($data)) {
997
                foreach ($data as $key => $val) {
998
                    if (isset($val[$user['user_id']])) {
999
                        if (is_array($val[$user['user_id']])) {
1000
                            $userRow[$defaultExtraFieldInfo[$key]['variable']] = implode(
1001
                                ', ',
1002
                                $val[$user['user_id']]
1003
                            );
1004
                        } else {
1005
                            $userRow[$defaultExtraFieldInfo[$key]['variable']] = $val[$user['user_id']];
1006
                        }
1007
                    } else {
1008
                        $userRow[$defaultExtraFieldInfo[$key]['variable']] = '';
1009
                    }
1010
                }
1011
            }
1012
1013
            if (api_get_setting('show_email_addresses') === 'true') {
1014
                $userRow['email'] = $user['col4'];
1015
            }
1016
1017
            $userRow['link'] = $user['link'];
1018
1019
            if ($exportCsv) {
1020
                unset($userRow['link']);
1021
                $csvContent[] = $userRow;
1022
            }
1023
1024
            $users[] = array_values($userRow);
1025
        }
1026
1027
        if ($exportCsv) {
1028
            Session::write('csv_content', $csvContent);
1029
        }
1030
1031
        Session::erase('additional_user_profile_info');
1032
        Session::erase('extra_field_info');
1033
        Session::erase('default_additional_user_profile_info');
1034
        Session::erase('default_extra_field_info');
1035
        Session::write('user_id_list', $userIdList);
1036
1037
        return $users;
1038
    }
1039
1040
    /**
1041
     * Get data for users list in sortable with pagination.
1042
     */
1043
    public static function getTotalTimeReport(
1044
        $from,
1045
        $numberOfItems,
1046
        $column,
1047
        $direction,
1048
        bool $includeInvitedUsers = false
1049
    ): array {
1050
        global $user_ids, $course_code, $export_csv, $session_id;
1051
1052
        $course_code = Database::escape_string($course_code);
1053
        $tblUser = Database::get_main_table(TABLE_MAIN_USER);
1054
        $tblUrlRelUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1055
        $accessUrlId = api_get_current_access_url_id();
1056
1057
        // get all users data from a course for sortable with limit
1058
        if (is_array($user_ids)) {
1059
            $user_ids = array_map('intval', $user_ids);
1060
            $conditionUser = " WHERE user.user_id IN (".implode(',', $user_ids).") ";
1061
        } else {
1062
            $user_ids = intval($user_ids);
1063
            $conditionUser = " WHERE user.user_id = $user_ids ";
1064
        }
1065
1066
        $urlTable = null;
1067
        $urlCondition = null;
1068
        if (api_is_multiple_url_enabled()) {
1069
            $urlTable = ", ".$tblUrlRelUser." as url_users";
1070
            $urlCondition = " AND user.user_id = url_users.user_id AND access_url_id='$accessUrlId'";
1071
        }
1072
1073
        $invitedUsersCondition = '';
1074
        if (!$includeInvitedUsers) {
1075
            $invitedUsersCondition = " AND user.status != ".INVITEE;
1076
        }
1077
1078
        $sql = "SELECT  user.user_id as user_id,
1079
                    user.official_code  as col0,
1080
                    user.lastname       as col1,
1081
                    user.firstname      as col2,
1082
                    user.username       as col3
1083
                FROM $tblUser as user $urlTable
1084
                $conditionUser $urlCondition $invitedUsersCondition";
1085
1086
        if (!in_array($direction, ['ASC', 'DESC'])) {
1087
            $direction = 'ASC';
1088
        }
1089
1090
        $column = (int) $column;
1091
        $from = (int) $from;
1092
        $numberOfItems = (int) $numberOfItems;
1093
1094
        $sql .= " ORDER BY col$column $direction ";
1095
        $sql .= " LIMIT $from,$numberOfItems";
1096
1097
        $res = Database::query($sql);
1098
        $users = [];
1099
1100
        $sortByFirstName = api_sort_by_first_name();
1101
        $courseInfo = api_get_course_info($course_code);
1102
        $courseId = $courseInfo['real_id'];
1103
1104
        while ($user = Database::fetch_array($res, 'ASSOC')) {
1105
            $user['official_code'] = $user['col0'];
1106
            $user['lastname'] = $user['col1'];
1107
            $user['firstname'] = $user['col2'];
1108
            $user['username'] = $user['col3'];
1109
1110
            $totalCourseTime = Tracking::get_time_spent_on_the_course(
1111
                $user['user_id'],
1112
                $courseId,
1113
                $session_id
1114
            );
1115
1116
            $user['time'] = api_time_to_hms($totalCourseTime);
1117
            $totalLpTime = Tracking::get_time_spent_in_lp(
1118
                $user['user_id'],
1119
                api_get_course_entity($courseId),
1120
                [],
1121
                $session_id
1122
            );
1123
1124
            $warning = '';
1125
            if ($totalLpTime > $totalCourseTime) {
1126
                $warning = '&nbsp;'.Display::label(get_lang('Time difference'), 'danger');
1127
            }
1128
1129
            $user['total_lp_time'] = api_time_to_hms($totalLpTime).$warning;
1130
1131
            $user['first_connection'] = Tracking::get_first_connection_date_on_the_course(
1132
                $user['user_id'],
1133
                $courseId,
1134
                $session_id
1135
            );
1136
            $user['last_connection'] = Tracking::get_last_connection_date_on_the_course(
1137
                $user['user_id'],
1138
                $courseInfo,
1139
                $session_id,
1140
                $export_csv === false
1141
            );
1142
1143
            $user['link'] = '<center>
1144
                             <a href="../my_space/myStudents.php?student='.$user['user_id'].'&details=true&cid='.$courseId.'&sid='.$session_id.'&course='.$course_code.'&origin=tracking_course&id_session='.$session_id.'">
1145
                             '.Display::return_icon('2rightarrow.png', get_lang('Details')).'
1146
                             </a>
1147
                         </center>';
1148
1149
            // store columns in array $users
1150
            $userRow = [];
1151
            $userRow['official_code'] = $user['official_code']; //0
1152
            if ($sortByFirstName) {
1153
                $userRow['firstname'] = $user['firstname'];
1154
                $userRow['lastname'] = $user['lastname'];
1155
            } else {
1156
                $userRow['lastname'] = $user['lastname'];
1157
                $userRow['firstname'] = $user['firstname'];
1158
            }
1159
            $userRow['username'] = $user['username'];
1160
            $userRow['time'] = $user['time'];
1161
            $userRow['total_lp_time'] = $user['total_lp_time'];
1162
            $userRow['first_connection'] = $user['first_connection'];
1163
            $userRow['last_connection'] = $user['last_connection'];
1164
1165
            $userRow['link'] = $user['link'];
1166
            $users[] = array_values($userRow);
1167
        }
1168
1169
        return $users;
1170
    }
1171
1172
    /**
1173
     * Determines the remaining actions for a session and returns a string with the results.
1174
     */
1175
    public static function actionsLeft(
1176
        string $current,
1177
        int $sessionId = 0,
1178
        bool $showExtended = false
1179
    ): string {
1180
        // Keep all course/session params consistent across tabs
1181
        $cidReq      = api_get_cidreq(true, false);
1182
        $cidQuery    = $cidReq ? ('?'.$cidReq) : '';
1183
        $webCodePath = api_get_path(WEB_CODE_PATH);
1184
1185
        $items = [
1186
            'users' => [
1187
                'icon'  => ToolIcon::MEMBER,
1188
                'label' => get_lang('Report on learners'),
1189
                'url'   => 'courseLog.php'.$cidQuery,
1190
            ],
1191
            'groups' => [
1192
                'icon'  => ToolIcon::GROUP,
1193
                'label' => get_lang('Group reporting'),
1194
                'url'   => 'course_log_groups.php'.$cidQuery,
1195
            ],
1196
            'resources' => [
1197
                'icon'  => ToolIcon::DOCUMENT,
1198
                'label' => get_lang('Report on resources'),
1199
                'url'   => 'course_log_resources.php'.$cidQuery,
1200
            ],
1201
            'courses' => [
1202
                'icon'  => ToolIcon::COURSE,
1203
                'label' => get_lang('Course report'),
1204
                'url'   => 'course_log_tools.php'.$cidQuery,
1205
            ],
1206
            'exams' => [
1207
                'icon'  => ToolIcon::QUIZ,
1208
                'label' => get_lang('Exam tracking'),
1209
                'url'   => $webCodePath.'tracking/exams.php'.$cidQuery,
1210
            ],
1211
            'logs' => [
1212
                'icon'  => ToolIcon::SECURITY,
1213
                'label' => get_lang('Audit report'),
1214
                'url'   => $webCodePath.'tracking/course_log_events.php'.$cidQuery,
1215
            ],
1216
            'lp' => [
1217
                'icon'  => ToolIcon::LP,
1218
                'label' => get_lang('Learning paths generic stats'),
1219
                'url'   => $webCodePath.'tracking/lp_report.php'.$cidQuery,
1220
            ],
1221
        ];
1222
1223
        if (!empty($sessionId)) {
1224
            $items['attendance'] = [
1225
                'icon'  => ToolIcon::ATTENDANCE,
1226
                'label' => get_lang('Logins'),
1227
                'url'   => $webCodePath.'attendance/index.php'.$cidQuery.'&action=calendar_logins',
1228
            ];
1229
        }
1230
1231
        // ---------------------------------------------------------------------
1232
        // Hide tabs that require a single course context when there is no course
1233
        // or when we are in "global" reporting mode (showExtended = true).
1234
        // This avoids linking to courseLog.php which will block with api_not_allowed().
1235
        // ---------------------------------------------------------------------
1236
        $hasCourse = null !== api_get_course_entity();
1237
        $isGlobalContext = $showExtended || !$hasCourse;
1238
1239
        if ($isGlobalContext) {
1240
            unset($items['users'], $items['lp']);
1241
        }
1242
1243
        $links = [];
1244
1245
        foreach ($items as $key => $config) {
1246
            $isCurrent = ($key === $current);
1247
1248
            // Icon inside the pill
1249
            $iconHtml = Display::getMdiIcon(
1250
                $config['icon'],
1251
                'ch-tool-icon course-log-tab-icon',
1252
                null,
1253
                ICON_SIZE_SMALL,
1254
                $config['label']
1255
            );
1256
1257
            // Base classes for tab look & feel
1258
            $tabClass = 'course-log-tab inline-flex items-center gap-2 px-3 py-1 rounded-full transition ';
1259
1260
            if ($isCurrent) {
1261
                // Active pill
1262
                $tabClass .= 'admin-report-card-active text-gray-90 shadow-sm';
1263
            } else {
1264
                // Inactive pill
1265
                $tabClass .= 'text-gray-60 hover:bg-gray-15 hover:text-gray-90';
1266
            }
1267
1268
            $attrs = [
1269
                'class' => $tabClass,
1270
                'title' => $config['label'],
1271
            ];
1272
1273
            $href = $isCurrent ? '#' : $config['url'];
1274
1275
            $links[] = Display::url(
1276
                $iconHtml.'<span>'.Security::remove_XSS($config['label']).'</span>',
1277
                $href,
1278
                $attrs
1279
            );
1280
        }
1281
1282
        // Horizontal pill container (tabs)
1283
        return
1284
            '<nav class="course-log-nav inline-flex flex-wrap items-center gap-1 '.
1285
            'rounded-full bg-gray-10 border border-gray-25 px-1 py-1 text-body-2">'.
1286
            implode('', $links).
1287
            '</nav>';
1288
    }
1289
1290
    public static function calcBestScoreAverageNotInLP(
1291
        array $exerciseList,
1292
        array $usersInGroup,
1293
        int $cId,
1294
        int $sessionId = 0,
1295
        bool $returnFormatted = false
1296
    ) {
1297
        if (empty($exerciseList) || empty($usersInGroup)) {
1298
            return 0;
1299
        }
1300
1301
        $bestScoreAverageNotInLP = 0;
1302
1303
        foreach ($exerciseList as $exerciseData) {
1304
            foreach ($usersInGroup as $userId) {
1305
                $results = Event::get_best_exercise_results_by_user(
1306
                    $exerciseData['iid'],
1307
                    $cId,
1308
                    $sessionId,
1309
                    $userId
1310
                );
1311
1312
                $scores = array_map(
1313
                    function (array $result) {
1314
                        return empty($result['exe_weighting']) ? 0 : $result['exe_result'] / $result['exe_weighting'];
1315
                    },
1316
                    $results
1317
                );
1318
1319
                $bestScoreAverageNotInLP += $scores ? max($scores) : 0;
1320
            }
1321
        }
1322
1323
        $rounded = round(
1324
            $bestScoreAverageNotInLP / count($exerciseList) * 100 / count($usersInGroup),
1325
            2
1326
        );
1327
1328
        if ($returnFormatted) {
1329
            return sprintf(get_lang('%s %%'), $rounded);
1330
        }
1331
1332
        return $rounded;
1333
    }
1334
1335
    /**
1336
     * Map resource_type.title to legacy "tool" identifier used in old tracking code.
1337
     */
1338
    private static function mapResourceTypeTitleToLegacyTool(string $title): ?string
1339
    {
1340
        static $map = [
1341
            'files'               => 'document',
1342
            'lps'                 => 'learnpath',
1343
            'exercises'           => 'quiz',
1344
            'glossaries'          => 'glossary',
1345
            'links'               => 'link',
1346
            'course_descriptions' => 'course_description',
1347
            'announcements'       => 'announcement',
1348
            'thematics'           => 'thematic',
1349
            'thematic_advance'    => 'thematic_advance',
1350
            'thematic_plan'       => 'thematic_plan',
1351
        ];
1352
1353
        $title = trim($title);
1354
1355
        return $map[$title] ?? null;
1356
    }
1357
}
1358