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

TrackingCourseLog::getUserData()   F

Complexity

Conditions 70

Size

Total Lines 536
Code Lines 329

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 70
eloc 329
nop 11
dl 0
loc 536
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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