Passed
Push — master ( 1c618d...c1c6b0 )
by Yannick
10:36 queued 02:52
created

Event::get_all_exercise_results()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 41
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 22
nc 8
nop 5
dl 0
loc 41
rs 9.2568
c 0
b 0
f 0
1
<?php
2
3
/* See license terms in /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\Course as CourseEntity;
7
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
8
use Chamilo\CoreBundle\Entity\TrackEAttemptQualify;
9
use Chamilo\CoreBundle\Entity\TrackEDefault;
10
use Chamilo\CoreBundle\Entity\TrackEDownloads;
11
use Chamilo\CoreBundle\Entity\TrackEExercise;
12
use Chamilo\CoreBundle\Entity\User;
13
use Chamilo\CoreBundle\Framework\Container;
14
use ChamiloSession as Session;
15
use Doctrine\ORM\Exception\ORMException;
16
use Doctrine\ORM\Exception\NotSupported;
17
18
/**
19
 * Class Event
20
 * Functions of this library are used to record information when some kind
21
 * of event occur. Each event has his own types of information then each event
22
 * use its own function.
23
 */
24
class Event
25
{
26
    /**
27
     * Record information for login event when a user identifies himself with username & password
28
     * @param int $userId
29
     *
30
     * @return bool
31
     *
32
     * @throws Exception
33
     * @author Julio Montoya
34
     * @author Sebastien Piraux <[email protected]> old code
35
     */
36
    public static function eventLogin(int $userId): bool
37
    {
38
        $userInfo = api_get_user_info($userId);
39
40
        if (empty($userInfo)) {
41
            return false;
42
        }
43
44
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
45
        $reallyNow = api_get_utc_datetime();
46
        $userIp = Database::escape_string(api_get_real_ip());
47
48
        $sql = "INSERT INTO $table (login_user_id, user_ip, login_date, logout_date) VALUES
49
                    ($userId,
50
                    '$userIp',
51
                    '$reallyNow',
52
                    '$reallyNow'
53
                )";
54
        Database::query($sql);
55
56
        $status = 'student';
57
        if (SESSIONADMIN == $userInfo['status']) {
58
            $status = 'sessionadmin';
59
        }
60
        if (COURSEMANAGER == $userInfo['status']) {
61
            $status = 'teacher';
62
        }
63
        if (DRH == $userInfo['status']) {
64
            $status = 'DRH';
65
        }
66
67
        // Auto subscribe
68
        $autoSubscribe = api_get_setting($status.'_autosubscribe');
69
        if ($autoSubscribe) {
70
            $autoSubscribe = explode('|', $autoSubscribe);
71
            foreach ($autoSubscribe as $code) {
72
                if (CourseManager::course_exists($code)) {
73
                    $courseInfo = api_get_course_info($code);
74
                    CourseManager::subscribeUser($userId, $courseInfo['real_id']);
75
                }
76
            }
77
        }
78
79
        return true;
80
    }
81
82
    /**
83
     * Check if we need to log a session access (based on visibility and extra field 'disable_log_after_session_ends')
84
     * @param int $sessionId
85
     *
86
     * @return bool
87
     */
88
    public static function isSessionLogNeedToBeSave(int $sessionId): bool
89
    {
90
        if (!empty($sessionId)) {
91
            $visibility = api_get_session_visibility($sessionId);
92
            if (!empty($visibility) && SESSION_AVAILABLE != $visibility) {
93
                $extraFieldValue = new ExtraFieldValue('session');
94
                $value = $extraFieldValue->get_values_by_handler_and_field_variable(
95
                    $sessionId,
96
                    'disable_log_after_session_ends'
97
                );
98
                if (!empty($value) && isset($value['value']) && 1 == (int) $value['value']) {
99
                    return false;
100
                }
101
            }
102
        }
103
104
        return true;
105
    }
106
107
    /**
108
     * Record information for access event for tools
109
     * @param string $tool name of the tool
110
     *
111
     * @return bool
112
     * @throws \Doctrine\DBAL\Exception
113
     * @throws \Doctrine\DBAL\Exception
114
     * @throws Exception
115
     * @author Sebastien Piraux <[email protected]>
116
     *
117
     *  $tool can take this values :
118
     *  Links, Calendar, Document, Announcements,
119
     *  Group, Video, Works, Users, Exercises, Course Desc
120
     *  ...
121
     *  Values can be added if new modules are created (15char max)
122
     *  I encourage to use $nameTool as $tool when calling this function
123
     *
124
     * Functionality for "what's new" notification is added by Toon Van Hoecke
125
     *
126
     */
127
    public static function event_access_tool(string $tool): bool
128
    {
129
        if (Session::read('login_as')) {
130
            return false;
131
        }
132
133
        $tool = Database::escape_string($tool);
134
135
        if (empty($tool)) {
136
            return false;
137
        }
138
139
        $courseInfo = api_get_course_info();
140
        $sessionId = api_get_session_id();
141
        $reallyNow = api_get_utc_datetime();
142
        $userId = api_get_user_id();
143
144
        if (empty($courseInfo)) {
145
            return false;
146
        }
147
148
        if (false === self::isSessionLogNeedToBeSave($sessionId)) {
149
            return false;
150
        }
151
152
        $courseId = $courseInfo['real_id'];
153
154
        $tableAccess = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ACCESS);
155
        //for "what's new" notification
156
        $tableLastAccess = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS);
157
158
        // record information
159
        // only if user comes from the course $_cid
160
        //if( eregi($_configuration['root_web'].$_cid,$_SERVER['HTTP_REFERER'] ) )
161
        //$pos = strpos($_SERVER['HTTP_REFERER'],$_configuration['root_web'].$_cid);
162
        $coursePath = isset($courseInfo['path']) ? $courseInfo['path'] : null;
163
164
        $pos = isset($_SERVER['HTTP_REFERER']) ? strpos(
165
            strtolower($_SERVER['HTTP_REFERER']),
166
            strtolower(api_get_path(WEB_COURSE_PATH).$coursePath)
167
        ) : false;
168
        // added for "what's new" notification
169
        $pos2 = isset($_SERVER['HTTP_REFERER']) ? strpos(
170
            strtolower($_SERVER['HTTP_REFERER']),
171
            strtolower(api_get_path(WEB_PATH)."index")
172
        ) : false;
173
174
        // end "what's new" notification
175
        if (false !== $pos || false !== $pos2) {
176
            $params = [
177
                'access_user_id' => $userId,
178
                'c_id' => $courseId,
179
                'access_tool' => $tool,
180
                'access_date' => $reallyNow,
181
                'session_id' => $sessionId,
182
                'user_ip' => Database::escape_string(api_get_real_ip()),
183
            ];
184
            Database::insert($tableAccess, $params);
185
        }
186
187
        // "what's new" notification
188
        $sql = "UPDATE $tableLastAccess
189
                SET access_date = '$reallyNow'
190
                WHERE
191
                    access_user_id = $userId AND
192
                    c_id = $courseId AND
193
                    access_tool = '$tool' AND
194
                    session_id = $sessionId";
195
        $result = Database::query($sql);
196
197
        if (0 == Database::affected_rows($result)) {
198
            $params = [
199
                'access_user_id' => $userId,
200
                'c_id' => $courseId,
201
                'access_tool' => $tool,
202
                'access_date' => $reallyNow,
203
                'session_id' => $sessionId,
204
            ];
205
            Database::insert($tableLastAccess, $params);
206
        }
207
208
        return true;
209
    }
210
211
    /**
212
     * Record information for download event (when a user clicks to d/l a
213
     * document).
214
     *
215
     * @param string $documentUrl
216
     *
217
     * @return int
218
     *
219
     * @throws NotSupported
220
     * @author Evie Embrechts
221
     * @author Sebastien Piraux <[email protected]>
222
     */
223
    public static function event_download(string $documentUrl): int
224
    {
225
        if (Session::read('login_as')) {
226
            return 0;
227
        }
228
229
        $user = api_get_user_entity();
230
        $course = api_get_course_entity();
231
232
        $em = Database::getManager();
233
234
        $repository = $em->getRepository(TrackEDownloads::class);
235
236
        $resourceLinkId = $course->getFirstResourceLink()->getId();
237
238
        return $repository->saveDownload($user->getId(), $resourceLinkId, $documentUrl);
239
    }
240
241
    /**
242
     * Record information of upload event.
243
     * Used in the works tool to record information when a user uploads 1 work.
244
     * @param int $documentId of document (id in mainDb.document table)
245
     *
246
     * @return int
247
     * @throws Exception
248
     * @author Sebastien Piraux <[email protected]>
249
     */
250
    public static function event_upload(int $documentId): int
251
    {
252
        if (Session::read('login_as')) {
253
            return 0;
254
        }
255
256
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_UPLOADS);
257
        $courseId = api_get_course_int_id();
258
        $reallyNow = api_get_utc_datetime();
259
        $userId = api_get_user_id();
260
        $sessionId = api_get_session_id();
261
262
        $sql = "INSERT INTO $table
263
                ( upload_user_id,
264
                  c_id,
265
                  upload_work_id,
266
                  upload_date,
267
                  session_id
268
                )
269
                VALUES (
270
                 $userId,
271
                 $courseId,
272
                 $documentId,
273
                 '$reallyNow',
274
                 $sessionId
275
                )";
276
        Database::query($sql);
277
278
        return 1;
279
    }
280
281
    /**
282
     * Record information for link event (when a user clicks on an added link).
283
     *
284
     * @param int $linkId (id in c_link table)
285
     *
286
     * @return int
287
     *
288
     * @throws Exception
289
     * @author Sebastien Piraux <[email protected]>
290
     */
291
    public static function event_link(int $linkId): int
292
    {
293
        if (Session::read('login_as')) {
294
            return 0;
295
        }
296
297
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LINKS);
298
        $reallyNow = api_get_utc_datetime();
299
        $userId = api_get_user_id();
300
        $courseId = api_get_course_int_id();
301
        $sessionId = api_get_session_id();
302
        $sql = "INSERT INTO ".$table."
303
                    ( links_user_id,
304
                     c_id,
305
                     links_link_id,
306
                     links_date,
307
                     session_id
308
                    ) VALUES (
309
                     $userId,
310
                     $courseId,
311
                     $linkId,
312
                     '$reallyNow',
313
                     $sessionId
314
                    )";
315
        Database::query($sql);
316
317
        return 1;
318
    }
319
320
    /**
321
     * Update the TRACK_E_EXERCICES exercises.
322
     * Record result of user when an exercise was done.
323
     *
324
     * @param int    $exeId
325
     * @param int    $exoId
326
     * @param float  $score
327
     * @param int    $weighting
328
     * @param int    $sessionId
329
     * @param ?int    $learnpathId
330
     * @param ?int    $learnpathItemId
331
     * @param ?int    $learnpathItemViewId
332
     * @param ?int    $duration
333
     * @param ?array  $questionsList
334
     * @param ?string $status
335
     * @param ?array  $remindList
336
     * @param ?string   $endDate
337
     *
338
     * @return bool
339
     *
340
     * @throws Exception
341
     * @author Sebastien Piraux <[email protected]>
342
     * @author Julio Montoya Armas <[email protected]> Reworked 2010
343
     */
344
    public static function updateEventExercise(
345
        int $exeId,
346
        int $exoId,
347
        float $score,
348
        int $weighting,
349
        int $sessionId,
350
        ?int $learnpathId = 0,
351
        ?int $learnpathItemId = 0,
352
        ?int $learnpathItemViewId = 0,
353
        ?int $duration = 0,
354
        ?array $questionsList = [],
355
        ?string $status = '',
356
        ?array $remindList = [],
357
        ?string $endDate = null
358
    ):bool
359
    {
360
        if (empty($exeId)) {
361
            return false;
362
        }
363
364
        if (empty($status)) {
365
            $status = '';
366
        } else {
367
            $status = Database::escape_string($status);
368
        }
369
370
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
371
372
        if (!empty($questionsList)) {
373
            $questionsList = array_map('intval', $questionsList);
374
        }
375
376
        if (!empty($remindList)) {
377
            $remindList = array_map('intval', $remindList);
378
            $remindList = array_filter($remindList);
379
            $remindList = implode(",", $remindList);
380
        } else {
381
            $remindList = '';
382
        }
383
384
        if (empty($endDate)) {
385
            $endDate = api_get_utc_datetime();
386
        }
387
        $score = Database::escape_string($score);
388
        $weighting = Database::escape_string($weighting);
389
        $questions = implode(',', $questionsList);
390
        $userIp = Database::escape_string(api_get_real_ip());
391
392
        $sql = "UPDATE $table SET
393
               exe_exo_id = $exoId,
394
               score = '$score',
395
               max_score = '$weighting',
396
               orig_lp_id = $learnpathId,
397
               orig_lp_item_id = $learnpathItemId,
398
               orig_lp_item_view_id = $learnpathItemViewId,
399
               exe_duration = $duration,
400
               exe_date = '$endDate',
401
               status = '$status',
402
               questions_to_check = '$remindList',
403
               data_tracking = '$questions',
404
               user_ip = '$userIp'
405
             WHERE exe_id = $exeId";
406
        Database::query($sql);
407
408
        return true;
409
    }
410
411
    /**
412
     * Record an event for this attempt at answering an exercise.
413
     * @param Exercise $exercise
414
     * @param float  $score Score achieved
415
     * @param string $answer Answer given
416
     * @param int    $question_id
417
     * @param int    $exe_id Exercise attempt ID a.k.a exe_id (from track_e_exercise)
418
     * @param int    $position
419
     * @param ?int    $exercise_id From c_quiz
420
     * @param ?bool   $updateResults
421
     * @param ?int    $questionDuration Time spent in seconds
422
     * @param ?string $fileName Filename (for audio answers - using nanogong)
423
     * @param ?int    $user_id The user who's going to get this score.
424
     * @param ?int    $course_id Default value of null means "get from context".
425
     * @param ?int    $session_id Default value of null means "get from context".
426
     * @param ?int    $learnpath_id (from c_lp table). Default value of null means "get from context".
427
     * @param ?int    $learnpath_item_id (from the c_lp_item table). Default value of null means "get from context".
428
     *
429
     * @return bool Result of the insert query
430
     * @throws Exception
431
     * @throws \Doctrine\DBAL\Exception
432
     */
433
    public static function saveQuestionAttempt(
434
        Exercise $exercise,
435
        float $score,
436
        string $answer,
437
        int $question_id,
438
        int $exe_id,
439
        int $position,
440
        ?int $exercise_id = 0,
441
        ?bool $updateResults = false,
442
        ?int $questionDuration = 0,
443
        ?string $fileName = null,
444
        ?int $user_id = null,
445
        ?int $course_id = null,
446
        ?int $session_id = null,
447
        ?int $learnpath_id = null,
448
        ?int $learnpath_item_id = null
449
    ) {
450
        global $debug;
451
        $questionDuration = (int) $questionDuration;
452
        $now = api_get_utc_datetime();
453
        $recordingLog = ('true' === api_get_setting('exercise.quiz_answer_extra_recording'));
454
455
        // check user_id or get from context
456
        if (empty($user_id)) {
457
            $user_id = api_get_user_id();
458
            // anonymous
459
            if (empty($user_id)) {
460
                $user_id = api_get_anonymous_id();
461
            }
462
        }
463
        // check session_id or get from context
464
        $session_id = (int) $session_id;
465
        if (empty($session_id)) {
466
            $session_id = api_get_session_id();
467
        }
468
        // check learnpath_id or get from context
469
        if (empty($learnpath_id)) {
470
            global $learnpath_id;
471
        }
472
        // check learnpath_item_id or get from context
473
        if (empty($learnpath_item_id)) {
474
            global $learnpath_item_id;
475
        }
476
477
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
478
479
        if ($debug) {
480
            error_log('----- entering saveQuestionAttempt() function ------');
481
            error_log("answer: $answer");
482
            error_log("score: $score");
483
            error_log("question_id : $question_id");
484
            error_log("position: $position");
485
        }
486
487
        //Validation in case of fraud with active control time
488
        if (!ExerciseLib::exercise_time_control_is_valid($exercise, $learnpath_id, $learnpath_item_id)) {
489
            if ($debug) {
490
                error_log("exercise_time_control_is_valid is false");
491
            }
492
            $score = 0;
493
            $answer = 0;
494
        }
495
496
        if (empty($question_id) || empty($exe_id) || empty($user_id)) {
497
            return false;
498
        }
499
500
        if (null === $answer) {
501
            $answer = '';
502
        }
503
        if (null === $score) {
504
            $score = 0;
505
        }
506
507
        $attempt = [
508
            'user_id' => $user_id,
509
            'question_id' => $question_id,
510
            'answer' => $answer,
511
            'marks' => $score,
512
            'position' => $position,
513
            'tms' => $now,
514
            'filename' => !empty($fileName) ? basename($fileName) : $fileName,
515
            'teacher_comment' => '',
516
            'seconds_spent' => $questionDuration,
517
        ];
518
519
        // Check if attempt exists.
520
        $sql = "SELECT exe_id FROM $TBL_TRACK_ATTEMPT
521
                WHERE
522
                    exe_id = $exe_id AND
523
                    user_id = $user_id AND
524
                    question_id = $question_id AND
525
                    position = $position";
526
        $result = Database::query($sql);
527
        $attemptData = [];
528
        if (Database::num_rows($result)) {
529
            $attemptData = Database::fetch_assoc($result);
530
            if (!$updateResults) {
531
                //The attempt already exist do not update use  update_event_exercise() instead
532
                return false;
533
            }
534
        } else {
535
            $attempt['exe_id'] = $exe_id;
536
        }
537
538
        if ($debug) {
539
            error_log("updateResults : $updateResults");
540
            error_log('Saving question attempt:');
541
            error_log($sql);
542
        }
543
544
        $em = Database::getManager();
545
        if (!$updateResults) {
546
            $attempt_id = Database::insert($TBL_TRACK_ATTEMPT, $attempt);
547
            $trackExercise = $em->find(TrackEExercise::class, $exe_id);
548
549
            if ($recordingLog) {
550
                $recording = new TrackEAttemptQualify();
551
                $recording
552
                    ->setTrackExercise($trackExercise)
553
                    ->setQuestionId($question_id)
554
                    ->setAnswer($answer)
555
                    ->setMarks((int) $score)
556
                    ->setAuthor(api_get_user_id())
557
                    ->setSessionId($session_id)
558
                ;
559
                $em->persist($recording);
560
                $em->flush();
561
            }
562
        } else {
563
            if ('true' === api_get_setting('exercise.allow_time_per_question')) {
564
                $attempt['seconds_spent'] = $questionDuration + (int) $attemptData['seconds_spent'];
565
            }
566
            Database::update(
567
                $TBL_TRACK_ATTEMPT,
568
                $attempt,
569
                [
570
                    'exe_id = ? AND question_id = ? AND user_id = ? ' => [
571
                        $exe_id,
572
                        $question_id,
573
                        $user_id,
574
                    ],
575
                ]
576
            );
577
578
            if ($recordingLog) {
579
                $repoTrackQualify = $em->getRepository(TrackEAttemptQualify::class);
580
                $trackQualify = $repoTrackQualify->findBy(
581
                    [
582
                        'exeId' => $exe_id,
583
                        'questionId' => $question_id,
584
                        'sessionId' => $session_id,
585
                    ]
586
                );
587
                $trackExercise = $em->find(TrackEExercise::class, $exe_id);
588
                /** @var TrackEAttemptQualify $trackQualify */
589
                $trackQualify
590
                    ->setTrackExercise($trackExercise)
591
                    ->setQuestionId($question_id)
592
                    ->setAnswer($answer)
593
                    ->setMarks((int) $score)
594
                    ->setAuthor(api_get_user_id())
595
                    ->setSessionId($session_id)
596
                ;
597
                $em->persist($trackQualify);
598
                $em->flush();
599
            }
600
            $attempt_id = $exe_id;
601
        }
602
603
        return $attempt_id;
604
    }
605
606
    /**
607
     * Record a hotspot spot for this attempt at answering a hotspot question.
608
     *
609
     * @param Exercise $exercise
610
     * @param int      $exeId
611
     * @param int      $questionId Question ID
612
     * @param int      $answerId Answer ID
613
     * @param int      $correct
614
     * @param string   $coords Coordinates of this point (e.g. 123;324)
615
     * @param bool     $updateResults
616
     * @param ?int     $exerciseId Deprecated param
617
     * @param ?int     $lpId
618
     * @param ?int     $lpItemId
619
     *
620
     * @return int Result of the insert query, or 0 on error
621
     *
622
     * @throws \Doctrine\DBAL\Exception
623
     * @throws Exception
624
     * @uses Course code and user_id from global scope $_cid and $_user
625
     */
626
    public static function saveExerciseAttemptHotspot(
627
        Exercise $exercise,
628
        int $exeId,
629
        int $questionId,
630
        int $answerId,
631
        int $correct,
632
        string $coords,
633
        bool $updateResults = false,
634
        ?int $exerciseId = 0,
635
        ?int $lpId = 0,
636
        ?int $lpItemId = 0
637
    ): int
638
    {
639
        $debug = false;
640
641
        if (!$updateResults) {
642
            // Validation in case of fraud with activated control time
643
            if (!ExerciseLib::exercise_time_control_is_valid($exercise, $lpId, $lpItemId)) {
644
                if ($debug) {
645
                    error_log('Attempt is fraud');
646
                }
647
                $correct = 0;
648
            }
649
        }
650
651
        if (empty($exeId)) {
652
            if ($debug) {
653
                error_log('exe id is empty');
654
            }
655
656
            return 0;
657
        }
658
659
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
660
        if ($updateResults) {
661
            if ($debug) {
662
                error_log("Insert hotspot results: exeId: $exeId correct: $correct");
663
            }
664
            $params = [
665
                'hotspot_correct' => $correct,
666
                'hotspot_coordinate' => $coords,
667
            ];
668
            $res = Database::update(
669
                $table,
670
                $params,
671
                [
672
                    'hotspot_user_id = ? AND hotspot_exe_id = ? AND hotspot_question_id = ? AND hotspot_answer_id = ? ' => [
673
                        api_get_user_id(),
674
                        $exeId,
675
                        $questionId,
676
                        $answerId,
677
                    ],
678
                ]
679
            );
680
            if (!$res) {
681
                return 0;
682
            }
683
684
            return $res;
685
        } else {
686
            if ($debug) {
687
                error_log("Insert hotspot results: exeId: $exeId correct: $correct");
688
            }
689
690
            $res = Database::insert(
691
                $table,
692
                [
693
                    'hotspot_user_id' => api_get_user_id(),
694
                    'c_id' => api_get_course_int_id(),
695
                    'hotspot_exe_id' => $exeId,
696
                    'hotspot_question_id' => $questionId,
697
                    'hotspot_answer_id' => $answerId,
698
                    'hotspot_correct' => $correct,
699
                    'hotspot_coordinate' => $coords,
700
                ]
701
            );
702
            if (!$res) {
703
                return 0;
704
            }
705
706
            return $res;
707
        }
708
    }
709
710
    /**
711
     * Records information for common (or admin) events (in the track_e_default table).
712
     *
713
     * @param string  $event_type Type of event
714
     * @param string  $event_value_type Type of value
715
     * @param mixed   $event_value Value (string, or array in the case of user info)
716
     * @param ?string $datetime Datetime (UTC) (defaults to null)
717
     * @param ?int    $user_id User ID (defaults to null)
718
     * @param ?int    $course_id Course ID (defaults to null)
719
     * @param ?int    $sessionId Session ID
720
     *
721
     * @return bool
722
     * @assert ('','','') === false
723
     * @throws ORMException
724
     * @throws Exception
725
     * @author Yannick Warnier <[email protected]>
726
     *
727
     */
728
    public static function addEvent(
729
        string $event_type,
730
        string $event_value_type,
731
        mixed $event_value,
732
        ?string $datetime = null,
733
        ?int $user_id = null,
734
        ?int $course_id = null,
735
        ?int $sessionId = 0
736
    ): bool
737
    {
738
        if (empty($event_type)) {
739
            return false;
740
        }
741
        if (empty($course_id)) {
742
            $course_id = api_get_course_int_id();
743
        }
744
        if (empty($sessionId)) {
745
            $sessionId = api_get_session_id();
746
        }
747
748
        // Clean the user_info
749
        if (LOG_USER_OBJECT == $event_value_type) {
750
            if (is_array($event_value)) {
751
                unset($event_value['complete_name']);
752
                unset($event_value['complete_name_with_username']);
753
                unset($event_value['firstName']);
754
                unset($event_value['lastName']);
755
                unset($event_value['avatar_small']);
756
                unset($event_value['avatar']);
757
                unset($event_value['mail']);
758
                unset($event_value['password']);
759
                unset($event_value['last_login']);
760
                unset($event_value['picture_uri']);
761
                $event_value = serialize($event_value);
762
            }
763
764
            if ($event_value instanceof User) {
765
                $event_value = serialize(
766
                    [
767
                        'id' => $event_value->getId(),
768
                        'username' => $event_value->getUsername(),
769
                        'firstname' => $event_value->getFirstName(),
770
                        'lastname' => $event_value->getLastname(),
771
                    ]
772
                );
773
            }
774
        }
775
        // If event is an array then the $event_value_type should finish with
776
        // the suffix _array for example LOG_WORK_DATA = work_data_array
777
        if (is_array($event_value)) {
778
            $event_value = serialize($event_value);
779
        }
780
781
        $sessionId = empty($sessionId) ? api_get_session_id() : $sessionId;
782
783
        if (!isset($datetime)) {
784
            $datetime = api_get_utc_datetime();
785
        }
786
787
        if (!isset($user_id)) {
788
            $user_id = api_get_user_id();
789
        }
790
791
        $track = (new TrackEDefault())
792
            ->setDefaultUserId($user_id)
793
            ->setCId($course_id)
794
            ->setDefaultDate(new DateTime($datetime, new DateTimeZone('UTC')))
795
            ->setDefaultEventType($event_type)
796
            ->setDefaultValueType($event_value_type)
797
            ->setDefaultValue($event_value)
798
            ->setSessionId($sessionId);
799
800
        $em = Database::getManager();
801
        $em->persist($track);
802
        $em->flush();
803
804
        return true;
805
    }
806
807
    /**
808
     * Gets the last attempt of an exercise based in the exe_id.
809
     *
810
     * @param int $exeId
811
     *
812
     * @return string
813
     * @throws \Doctrine\DBAL\Exception
814
     * @throws Exception
815
     */
816
    public static function getLastAttemptDateOfExercise(int $exeId): string
817
    {
818
        $track_attempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
819
        $sql = "SELECT max(tms) as last_attempt_date
820
                FROM $track_attempts
821
                WHERE exe_id = $exeId";
822
        $rs_last_attempt = Database::query($sql);
823
        if (0 == Database::num_rows($rs_last_attempt)) {
824
            return '';
825
        }
826
        $row_last_attempt = Database::fetch_array($rs_last_attempt);
827
828
        return $row_last_attempt['last_attempt_date']; //Get the date of last attempt
829
    }
830
831
    /**
832
     * Gets the last attempt of an exercise based in the exe_id.
833
     *
834
     * @param int $exeId
835
     *
836
     * @return int
837
     * @throws \Doctrine\DBAL\Exception
838
     * @throws Exception
839
     */
840
    public static function getLatestQuestionIdFromAttempt(int $exeId): int
841
    {
842
        $track_attempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
843
        $sql = "SELECT question_id FROM $track_attempts
844
                WHERE exe_id = $exeId
845
                ORDER BY tms DESC
846
                LIMIT 1";
847
        $result = Database::query($sql);
848
        if (Database::num_rows($result)) {
849
            $row = Database::fetch_array($result);
850
851
            return $row['question_id'];
852
        }
853
854
        return 0;
855
    }
856
857
    /**
858
     * Gets how many attempts exists by user, exercise, learning path.
859
     *
860
     * @param int $user_id
861
     * @param int $exerciseId
862
     * @param int $lp_id
863
     * @param int $lp_item_id
864
     * @param int $lp_item_view_id
865
     *
866
     * @return int
867
     * @throws \Doctrine\DBAL\Exception
868
     * @throws Exception
869
     */
870
    public static function get_attempt_count(
871
        int $user_id,
872
        int $exerciseId,
873
        int $lp_id,
874
        int $lp_item_id,
875
        int $lp_item_view_id
876
    ): int
877
    {
878
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
879
        $courseId = api_get_course_int_id();
880
        $sessionId = api_get_session_id();
881
        $sessionCondition = api_get_session_condition($sessionId);
882
        $sql = "SELECT count(*) as count
883
                FROM $table
884
                WHERE
885
                    exe_exo_id = $exerciseId AND
886
                    exe_user_id = $user_id AND
887
                    status != 'incomplete' AND
888
                    orig_lp_id = $lp_id AND
889
                    orig_lp_item_id = $lp_item_id AND
890
                    orig_lp_item_view_id = $lp_item_view_id AND
891
                    c_id = $courseId
892
                    $sessionCondition";
893
894
        $result = Database::query($sql);
895
        if (Database::num_rows($result) > 0) {
896
            $attempt = Database::fetch_assoc($result);
897
898
            return (int) $attempt['count'];
899
        }
900
901
        return 0;
902
    }
903
904
    /**
905
     * Find the order (not the count) of the given attempt in the queue of attempts
906
     * @param int $exeId The attempt ID from track_e_exercises
907
     * @param int $user_id
908
     * @param int $exerciseId
909
     * @param int $lp_id
910
     * @param int $lp_item_id
911
     * @param int $lp_item_view_id
912
     *
913
     * @return int
914
     * @throws \Doctrine\DBAL\Exception
915
     * @throws Exception
916
     */
917
    public static function getAttemptPosition(
918
        int $exeId,
919
        int $user_id,
920
        int $exerciseId,
921
        int $lp_id,
922
        int $lp_item_id,
923
        int $lp_item_view_id
924
    ): int
925
    {
926
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
927
        $courseId = api_get_course_int_id();
928
        $sessionId = api_get_session_id();
929
930
        $sessionCondition = api_get_session_condition($sessionId);
931
        // Select all matching attempts
932
        $sql = "SELECT exe_id
933
                FROM $table
934
                WHERE
935
                    exe_exo_id = $exerciseId AND
936
                    exe_user_id = $user_id AND
937
                    status = '' AND
938
                    orig_lp_id = $lp_id AND
939
                    orig_lp_item_id = $lp_item_id AND
940
                    orig_lp_item_view_id = $lp_item_view_id AND
941
                    c_id = $courseId
942
                    $sessionCondition
943
                ORDER by exe_id
944
                ";
945
946
        $result = Database::query($sql);
947
        // Scroll through them until we found ours, to locate its order in the queue
948
        if (Database::num_rows($result) > 0) {
949
            $position = 1;
950
            while ($row = Database::fetch_assoc($result)) {
951
                if ($row['exe_id'] === $exeId) {
952
                    break;
953
                }
954
                $position++;
955
            }
956
957
            return $position;
958
        }
959
960
        return 0;
961
    }
962
963
    /**
964
     * @param int   $user_id
965
     * @param int   $lp_id
966
     * @param array $course
967
     * @param int   $session_id
968
     * @param ?bool $disconnectExerciseResultsFromLp (Replace orig_lp_* variables to null)
969
     *
970
     * @return bool
971
     * @throws Exception
972
     * @throws \Doctrine\DBAL\Exception
973
     * @throws ORMException
974
     */
975
    public static function delete_student_lp_events(
976
        int $user_id,
977
        int $lp_id,
978
        array $course,
979
        int $session_id,
980
        ?bool $disconnectExerciseResultsFromLp = false
981
    ): bool
982
    {
983
        $lp_view_table = Database::get_course_table(TABLE_LP_VIEW);
984
        $lp_item_view_table = Database::get_course_table(TABLE_LP_ITEM_VIEW);
985
        $lpInteraction = Database::get_course_table(TABLE_LP_IV_INTERACTION);
986
        $lpObjective = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
987
988
        if (empty($course) || empty($user_id)) {
989
            return false;
990
        }
991
992
        $course_id = $course['real_id'];
993
994
        if (empty($course_id)) {
995
            $course_id = api_get_course_int_id();
996
        }
997
998
        $track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
999
        $track_attempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1000
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
1001
        $sessionCondition = api_get_session_condition($session_id);
1002
        // Make sure we have the exact lp_view_id
1003
        $sql = "SELECT iid FROM $lp_view_table
1004
                WHERE
1005
                    c_id = $course_id AND
1006
                    user_id = $user_id AND
1007
                    lp_id = $lp_id
1008
                    $sessionCondition";
1009
        $result = Database::query($sql);
1010
1011
        if (Database::num_rows($result)) {
1012
            $view = Database::fetch_assoc($result);
1013
            $lp_view_id = $view['iid'];
1014
1015
            $sql = "DELETE FROM $lp_item_view_table
1016
                    WHERE lp_view_id = $lp_view_id";
1017
            Database::query($sql);
1018
1019
            $sql = "DELETE FROM $lpInteraction
1020
                    WHERE c_id = $course_id AND lp_iv_id = $lp_view_id";
1021
            Database::query($sql);
1022
1023
            $sql = "DELETE FROM $lpObjective
1024
                    WHERE c_id = $course_id AND lp_iv_id = $lp_view_id";
1025
            Database::query($sql);
1026
        }
1027
1028
        if ('true' === api_get_setting('lp.lp_minimum_time')) {
1029
            $sql = "DELETE FROM track_e_access_complete
1030
                    WHERE
1031
                        tool = 'learnpath' AND
1032
                        c_id = $course_id AND
1033
                        tool_id = $lp_id AND
1034
                        user_id = $user_id AND
1035
                        session_id = $session_id
1036
                    ";
1037
            Database::query($sql);
1038
        }
1039
1040
        $sql = "SELECT exe_id FROM $track_e_exercises
1041
                WHERE
1042
                    exe_user_id = $user_id AND
1043
                    session_id = $session_id AND
1044
                    c_id = $course_id AND
1045
                    orig_lp_id = $lp_id";
1046
        $result = Database::query($sql);
1047
        $exeList = [];
1048
        while ($row = Database::fetch_assoc($result)) {
1049
            $exeList[] = $row['exe_id'];
1050
        }
1051
1052
        if (!empty($exeList) && count($exeList) > 0) {
1053
            $exeListString = implode(',', $exeList);
1054
            if ($disconnectExerciseResultsFromLp) {
1055
                $sql = "UPDATE $track_e_exercises
1056
                        SET orig_lp_id = null,
1057
                            orig_lp_item_id = null,
1058
                            orig_lp_item_view_id = null
1059
                        WHERE exe_id IN ($exeListString)";
1060
                Database::query($sql);
1061
            } else {
1062
                $sql = "DELETE FROM $track_e_exercises
1063
                    WHERE exe_id IN ($exeListString)";
1064
                Database::query($sql);
1065
1066
                $sql = "DELETE FROM $track_attempts
1067
                    WHERE exe_id IN ($exeListString)";
1068
                Database::query($sql);
1069
1070
                $sql = "DELETE FROM $tblTrackAttemptQualify
1071
                    WHERE exe_id IN ($exeListString)";
1072
                Database::query($sql);
1073
            }
1074
        }
1075
1076
        $sql = "DELETE FROM $lp_view_table
1077
                WHERE
1078
                    c_id = $course_id AND
1079
                    user_id = $user_id AND
1080
                    lp_id= $lp_id AND
1081
                    session_id = $session_id
1082
            ";
1083
        Database::query($sql);
1084
1085
        self::addEvent(
1086
            LOG_LP_ATTEMPT_DELETE,
1087
            LOG_LP_ID,
1088
            $lp_id,
1089
            null,
1090
            null,
1091
            $course_id,
1092
            $session_id
1093
        );
1094
1095
        return true;
1096
    }
1097
1098
    /**
1099
     * Delete all exercise attempts (included in LP or not).
1100
     *
1101
     * @param int  $user_id
1102
     * @param int  $exercise_id
1103
     * @param int  $course_id
1104
     * @param ?int $session_id
1105
     * @throws ORMException
1106
     * @throws Exception
1107
     */
1108
    public static function delete_all_incomplete_attempts(
1109
        int $user_id,
1110
        int $exercise_id,
1111
        int $course_id,
1112
        ?int $session_id = 0
1113
    ): void
1114
    {
1115
        if (!empty($user_id) && !empty($exercise_id) && !empty($course_id)) {
1116
            $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1117
            $sessionCondition = api_get_session_condition($session_id);
1118
            $sql = "SELECT exe_id FROM $table
1119
                    WHERE
1120
                        exe_user_id = $user_id AND
1121
                        exe_exo_id = $exercise_id AND
1122
                        c_id = $course_id AND
1123
                        status = 'incomplete'
1124
                        $sessionCondition
1125
                        ";
1126
            $result = Database::query($sql);
1127
            $repo = Container::getTrackEExerciseRepository();
1128
            while ($row = Database::fetch_assoc($result)) {
1129
                $exeId = $row['exe_id'];
1130
                /** @var TrackEExercise $track */
1131
                $track = $repo->find($exeId);
1132
1133
                self::addEvent(
1134
                    LOG_EXERCISE_RESULT_DELETE,
1135
                    LOG_EXERCISE_AND_USER_ID,
1136
                    ($track->getQuiz()?->getIid()).'-'.$track->getUser()->getId(),
1137
                    null,
1138
                    null,
1139
                    $course_id,
1140
                    $session_id
1141
                );
1142
                $repo->delete($track);
1143
            }
1144
        }
1145
    }
1146
1147
    /**
1148
     * Gets all exercise results (NO Exercises in LPs ) from a given exercise id, course, session.
1149
     *
1150
     * @param int $exercise_id
1151
     * @param int $courseId
1152
     * @param ?int $session_id
1153
     * @param ?bool $load_question_list
1154
     * @param ?int $user_id
1155
     *
1156
     * @return array with the results
1157
     * @throws \Doctrine\DBAL\Exception
1158
     * @throws Exception
1159
     */
1160
    public static function get_all_exercise_results(
1161
        int $exercise_id,
1162
        int $courseId,
1163
        ?int $session_id = 0,
1164
        ?bool $load_question_list = true,
1165
        ?int $user_id = null
1166
    ): array
1167
    {
1168
        $TABLETRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1169
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1170
1171
        $user_condition = null;
1172
        if (!empty($user_id)) {
1173
            $user_condition = "AND exe_user_id = $user_id ";
1174
        }
1175
        $sessionCondition = api_get_session_condition($session_id);
1176
        $sql = "SELECT * FROM $TABLETRACK_EXERCISES
1177
                WHERE
1178
                    status = ''  AND
1179
                    c_id = $courseId AND
1180
                    exe_exo_id = $exercise_id AND
1181
                    orig_lp_id = 0 AND
1182
                    orig_lp_item_id = 0
1183
                    $user_condition
1184
                    $sessionCondition
1185
                ORDER BY exe_id";
1186
        $res = Database::query($sql);
1187
        $list = [];
1188
        while ($row = Database::fetch_assoc($res)) {
1189
            $list[$row['exe_id']] = $row;
1190
            if ($load_question_list) {
1191
                $sql = "SELECT * FROM $TBL_TRACK_ATTEMPT
1192
                        WHERE exe_id = {$row['exe_id']}";
1193
                $res_question = Database::query($sql);
1194
                while ($row_q = Database::fetch_assoc($res_question)) {
1195
                    $list[$row['exe_id']]['question_list'][$row_q['question_id']] = $row_q;
1196
                }
1197
            }
1198
        }
1199
1200
        return $list;
1201
    }
1202
1203
    /**
1204
     * Gets all exercise results (NO Exercises in LPs ) from a given exercise id, course, session.
1205
     *
1206
     * @param int   $courseId
1207
     * @param ?int  $session_id
1208
     * @param ?bool $get_count
1209
     *
1210
     * @return mixed Array with the results or count if $get_count == true
1211
     * @throws \Doctrine\DBAL\Exception
1212
     * @throws Exception
1213
     */
1214
    public static function get_all_exercise_results_by_course(
1215
        int $courseId,
1216
        ?int $session_id = 0,
1217
        ?bool $get_count = true
1218
    ) {
1219
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1220
1221
        $select = '*';
1222
        if ($get_count) {
1223
            $select = 'count(*) as count';
1224
        }
1225
        $sessionCondition = api_get_session_condition($session_id);
1226
        $sql = "SELECT $select FROM $table_track_exercises
1227
                WHERE   status = ''  AND
1228
                        c_id = $courseId AND
1229
                        orig_lp_id = 0 AND
1230
                        orig_lp_item_id = 0
1231
                        $sessionCondition
1232
                ORDER BY exe_id";
1233
        $res = Database::query($sql);
1234
        if ($get_count) {
1235
            $row = Database::fetch_assoc($res);
1236
1237
            return $row['count'];
1238
        } else {
1239
            $list = [];
1240
            while ($row = Database::fetch_assoc($res)) {
1241
                $list[$row['exe_id']] = $row;
1242
            }
1243
1244
            return $list;
1245
        }
1246
    }
1247
1248
    /**
1249
     * Gets all exercise results (NO Exercises in LPs) from a given exercise id, course, session.
1250
     *
1251
     * @param int  $user_id
1252
     * @param int  $courseId
1253
     * @param ?int $session_id
1254
     *
1255
     * @return array with the results
1256
     * @throws \Doctrine\DBAL\Exception
1257
     * @throws Exception
1258
     */
1259
    public static function get_all_exercise_results_by_user(
1260
        int $user_id,
1261
        int $courseId,
1262
        ?int $session_id = 0
1263
    ): array
1264
    {
1265
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1266
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1267
1268
        $sessionCondition = api_get_session_condition($session_id);
1269
        $sql = "SELECT * FROM $table_track_exercises
1270
                WHERE
1271
                    status = '' AND
1272
                    exe_user_id = $user_id AND
1273
                    c_id = $courseId AND
1274
                    orig_lp_id = 0 AND
1275
                    orig_lp_item_id = 0
1276
                    $sessionCondition
1277
                ORDER by exe_id";
1278
1279
        $res = Database::query($sql);
1280
        $list = [];
1281
        while ($row = Database::fetch_assoc($res)) {
1282
            $list[$row['exe_id']] = $row;
1283
            $sql = "SELECT * FROM $table_track_attempt
1284
                    WHERE exe_id = {$row['exe_id']}";
1285
            $res_question = Database::query($sql);
1286
            while ($row_q = Database::fetch_assoc($res_question)) {
1287
                $list[$row['exe_id']]['question_list'][$row_q['question_id']] = $row_q;
1288
            }
1289
        }
1290
1291
        return $list;
1292
    }
1293
1294
    /**
1295
     * Gets exercise results (NO Exercises in LPs) from a given exercise id, course, session.
1296
     *
1297
     * @param int    $exe_id attempt id
1298
     * @param ?string $status
1299
     *
1300
     * @return array with the results
1301
     * @throws \Doctrine\DBAL\Exception
1302
     * @throws Exception
1303
     */
1304
    public static function get_exercise_results_by_attempt(int $exe_id, ?string $status = null): array
1305
    {
1306
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1307
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1308
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
1309
1310
        $status = Database::escape_string($status);
1311
1312
        $sql = "SELECT * FROM $table_track_exercises
1313
                WHERE status = '$status' AND exe_id = $exe_id";
1314
1315
        $res = Database::query($sql);
1316
        $list = [];
1317
        if (Database::num_rows($res)) {
1318
            $row = Database::fetch_assoc($res);
1319
1320
            //Checking if this attempt was revised by a teacher
1321
            $sql_revised = "SELECT exe_id FROM $tblTrackAttemptQualify
1322
                            WHERE author != '' AND exe_id = $exe_id
1323
                            LIMIT 1";
1324
            $res_revised = Database::query($sql_revised);
1325
            $row['attempt_revised'] = 0;
1326
            if (Database::num_rows($res_revised) > 0) {
1327
                $row['attempt_revised'] = 1;
1328
            }
1329
            $list[$exe_id] = $row;
1330
            $sql = "SELECT * FROM $table_track_attempt
1331
                    WHERE exe_id = $exe_id
1332
                    ORDER BY tms ASC";
1333
            $res_question = Database::query($sql);
1334
            while ($row_q = Database::fetch_assoc($res_question)) {
1335
                $list[$exe_id]['question_list'][$row_q['question_id']] = $row_q;
1336
            }
1337
        }
1338
1339
        return $list;
1340
    }
1341
1342
    /**
1343
     * Gets exercise results (NO Exercises in LPs) from a given user, exercise id, course, session, lp_id, lp_item_id.
1344
     *
1345
     * @param int     $user_id
1346
     * @param int     $exercise_id
1347
     * @param int     $courseId
1348
     * @param ?int    $session_id
1349
     * @param ?int    $lp_id
1350
     * @param ?int    $lp_item_id
1351
     * @param ?string $order asc or desc
1352
     *
1353
     * @return array with the results
1354
     * @throws Exception
1355
     * @throws \Doctrine\DBAL\Exception
1356
     */
1357
    public static function getExerciseResultsByUser(
1358
        int $user_id,
1359
        int $exercise_id,
1360
        int $courseId,
1361
        ?int $session_id = 0,
1362
        ?int $lp_id = 0,
1363
        ?int $lp_item_id = 0,
1364
        ?string $order = null
1365
    ): array
1366
    {
1367
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1368
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1369
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
1370
        if (!isset($lp_id)) {
1371
            $lp_id = '0';
1372
        }
1373
        if (!isset($lp_item_id)) {
1374
            $lp_item_id = '0';
1375
        }
1376
1377
        if (!in_array(strtolower($order), ['asc', 'desc'])) {
1378
            $order = 'asc';
1379
        }
1380
1381
        $sessionCondition = api_get_session_condition($session_id);
1382
1383
        $sql = "SELECT * FROM $table_track_exercises
1384
                WHERE
1385
                    status 			= '' AND
1386
                    exe_user_id 	= $user_id AND
1387
                    c_id 	        = $courseId AND
1388
                    exe_exo_id 		= $exercise_id AND
1389
                    orig_lp_id 		= $lp_id AND
1390
                    orig_lp_item_id = $lp_item_id
1391
                    $sessionCondition
1392
                ORDER by exe_id $order ";
1393
        $res = Database::query($sql);
1394
        $list = [];
1395
        while ($row = Database::fetch_assoc($res)) {
1396
            // Checking if this attempt was revised by a teacher
1397
            $exeId = $row['exe_id'];
1398
            $sql = "SELECT exe_id FROM $tblTrackAttemptQualify
1399
                    WHERE author != '' AND exe_id = $exeId
1400
                    LIMIT 1";
1401
            $res_revised = Database::query($sql);
1402
            $row['attempt_revised'] = 0;
1403
            if (Database::num_rows($res_revised) > 0) {
1404
                $row['attempt_revised'] = 1;
1405
            }
1406
            $row['total_percentage'] = $row['max_score'] > 0 ? ($row['score'] / $row['max_score']) * 100 : 0;
1407
            $list[$row['exe_id']] = $row;
1408
            $sql = "SELECT * FROM $table_track_attempt
1409
                    WHERE exe_id = $exeId";
1410
            $res_question = Database::query($sql);
1411
            while ($row_q = Database::fetch_assoc($res_question)) {
1412
                $list[$row['exe_id']]['question_list'][$row_q['question_id']][] = $row_q;
1413
            }
1414
        }
1415
1416
        return $list;
1417
    }
1418
1419
    /**
1420
     * Count exercise attempts (NO Exercises in LPs ) from a given exercise id, course, session.
1421
     *
1422
     * @param int $user_id
1423
     * @param int $exercise_id
1424
     * @param int $courseId
1425
     * @param int $session_id
1426
     *
1427
     * @return int with the results
1428
     * @throws \Doctrine\DBAL\Exception
1429
     * @throws Exception
1430
     */
1431
    public static function count_exercise_attempts_by_user(
1432
        int $user_id,
1433
        int $exercise_id,
1434
        int $courseId,
1435
        int $session_id = 0
1436
    ): int {
1437
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1438
        $sessionCondition = api_get_session_condition($session_id);
1439
        $sql = "SELECT count(*) as count
1440
                FROM $table
1441
                WHERE status = ''  AND
1442
                    exe_user_id = $user_id AND
1443
                    c_id = $courseId AND
1444
                    exe_exo_id = $exercise_id AND
1445
                    orig_lp_id = 0 AND
1446
                    orig_lp_item_id = 0
1447
                    $sessionCondition
1448
                ORDER BY exe_id";
1449
        $res = Database::query($sql);
1450
        $result = 0;
1451
        if (Database::num_rows($res) > 0) {
1452
            $row = Database::fetch_assoc($res);
1453
            $result = $row['count'];
1454
        }
1455
1456
        return $result;
1457
    }
1458
1459
    /**
1460
     * Gets all exercise BEST results attempts (NO Exercises in LPs)
1461
     * from a given exercise id, course, session per user.
1462
     *
1463
     * @param int  $exercise_id
1464
     * @param int  $courseId
1465
     * @param ?int $session_id
1466
     * @param ?int $userId
1467
     *
1468
     * @return array with the results
1469
     *
1470
     * @throws Exception
1471
     * @throws \Doctrine\DBAL\Exception
1472
     * @todo rename this function
1473
     */
1474
    public static function get_best_exercise_results_by_user(
1475
        int $exercise_id,
1476
        int $courseId,
1477
        ?int $session_id = 0,
1478
        ?int $userId = 0
1479
    ): array
1480
    {
1481
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1482
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1483
        $sessionCondition = api_get_session_condition($session_id);
1484
        $sql = "SELECT * FROM $table_track_exercises
1485
                WHERE
1486
                    status = '' AND
1487
                    c_id = $courseId AND
1488
                    exe_exo_id = $exercise_id AND
1489
                    orig_lp_id = 0 AND
1490
                    orig_lp_item_id = 0
1491
                    $sessionCondition
1492
                ";
1493
1494
        if (!empty($userId)) {
1495
            $sql .= " AND exe_user_id = $userId ";
1496
        }
1497
        $sql .= ' ORDER BY exe_id';
1498
1499
        $res = Database::query($sql);
1500
        $list = [];
1501
        while ($row = Database::fetch_assoc($res)) {
1502
            $list[$row['exe_id']] = $row;
1503
            $exeId = $row['exe_id'];
1504
            $sql = "SELECT * FROM $table_track_attempt
1505
                    WHERE exe_id = $exeId";
1506
            $res_question = Database::query($sql);
1507
            while ($row_q = Database::fetch_assoc($res_question)) {
1508
                $list[$exeId]['question_list'][$row_q['question_id']] = $row_q;
1509
            }
1510
        }
1511
1512
        // Getting the best results of every student
1513
        $best_score_return = [];
1514
        foreach ($list as $student_result) {
1515
            $user_id = $student_result['exe_user_id'];
1516
            $current_best_score[$user_id] = $student_result['score'];
1517
            if (!isset($best_score_return[$user_id]['score'])) {
1518
                $best_score_return[$user_id] = $student_result;
1519
            }
1520
1521
            if ($current_best_score[$user_id] > $best_score_return[$user_id]['score']) {
1522
                $best_score_return[$user_id] = $student_result;
1523
            }
1524
        }
1525
1526
        return $best_score_return;
1527
    }
1528
1529
    /**
1530
     * Get the last best result from all attempts in exercises per user (out of learning paths).
1531
     *
1532
     * @param int  $user_id
1533
     * @param int  $exercise_id
1534
     * @param int  $courseId
1535
     * @param ?int  $session_id
1536
     * @param ?bool $skipLpResults
1537
     *
1538
     * @return array
1539
     * @throws Exception
1540
     * @throws \Doctrine\DBAL\Exception
1541
     */
1542
    public static function get_best_attempt_exercise_results_per_user(
1543
        int $user_id,
1544
        int $exercise_id,
1545
        int $courseId,
1546
        ?int $session_id = 0,
1547
        ?bool $skipLpResults = true
1548
    ):array
1549
    {
1550
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1551
1552
        $sessionCondition = api_get_session_condition($session_id);
1553
        $sql = "SELECT * FROM $table
1554
                WHERE
1555
                    status = ''  AND
1556
                    c_id = $courseId AND
1557
                    exe_exo_id = $exercise_id AND
1558
                    exe_user_id = $user_id
1559
                    $sessionCondition
1560
                ";
1561
1562
        if ($skipLpResults) {
1563
            $sql .= ' AND
1564
                    orig_lp_id = 0 AND
1565
                orig_lp_item_id = 0 ';
1566
        }
1567
1568
        $sql .= ' ORDER BY exe_id ';
1569
1570
        $res = Database::query($sql);
1571
        $list = [];
1572
        while ($row = Database::fetch_assoc($res)) {
1573
            $list[$row['exe_id']] = $row;
1574
        }
1575
        //Getting the best results of every student
1576
        $best_score_return = [];
1577
        $best_score_return['score'] = 0;
1578
1579
        foreach ($list as $result) {
1580
            $current_best_score = $result;
1581
            if ($current_best_score['score'] > $best_score_return['score']) {
1582
                $best_score_return = $result;
1583
            }
1584
        }
1585
        if (!isset($best_score_return['max_score'])) {
1586
            $best_score_return = [];
1587
        }
1588
1589
        return $best_score_return;
1590
    }
1591
1592
    /**
1593
     * Gets all exercise events from a Learning Path within a Course    nd Session.
1594
     *
1595
     * @param int $exercise_id
1596
     * @param int $courseId
1597
     * @param ?int $session_id
1598
     *
1599
     * @return array
1600
     * @throws Exception
1601
     * @throws \Doctrine\DBAL\Exception
1602
     */
1603
    public static function get_all_exercise_event_from_lp(
1604
        int $exercise_id,
1605
        int $courseId,
1606
        ?int $session_id = 0
1607
    ): array
1608
    {
1609
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1610
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1611
        $sessionCondition = api_get_session_condition($session_id);
1612
1613
        $sql = "SELECT * FROM $table_track_exercises
1614
                WHERE
1615
                    status = '' AND
1616
                    c_id = $courseId AND
1617
                    exe_exo_id = $exercise_id AND
1618
                    orig_lp_id !=0 AND
1619
                    orig_lp_item_id != 0
1620
                    $sessionCondition
1621
                    ";
1622
1623
        $res = Database::query($sql);
1624
        $list = [];
1625
        while ($row = Database::fetch_assoc($res)) {
1626
            $exeId = $row['exe_id'];
1627
            $list[$exeId] = $row;
1628
            $sql = "SELECT * FROM $table_track_attempt
1629
                    WHERE exe_id = $exeId";
1630
            $res_question = Database::query($sql);
1631
            while ($row_q = Database::fetch_assoc($res_question)) {
1632
                $list[$exeId]['question_list'][$row_q['question_id']] = $row_q;
1633
            }
1634
        }
1635
1636
        return $list;
1637
    }
1638
1639
    /**
1640
     * Get a list of all the exercises in a given learning path.
1641
     *
1642
     * @param int $lp_id
1643
     *
1644
     * @return array
1645
     * @throws \Doctrine\DBAL\Exception
1646
     * @throws Exception
1647
     */
1648
    public static function get_all_exercises_from_lp(int $lp_id): array
1649
    {
1650
        $lp_item_table = Database::get_course_table(TABLE_LP_ITEM);
1651
        $sql = "SELECT * FROM $lp_item_table
1652
                WHERE
1653
                    lp_id = $lp_id AND
1654
                    item_type = 'quiz'
1655
                ORDER BY parent_item_id, display_order";
1656
        $res = Database::query($sql);
1657
1658
        $list = [];
1659
        while ($row = Database::fetch_assoc($res)) {
1660
            $list[] = $row;
1661
        }
1662
1663
        return $list;
1664
    }
1665
1666
    /**
1667
     * This function gets the comments of an exercise.
1668
     *
1669
     * @param int $exe_id
1670
     * @param int $question_id
1671
     *
1672
     * @return string the comment
1673
     * @throws \Doctrine\DBAL\Exception
1674
     * @throws Exception
1675
     */
1676
    public static function get_comments(int $exe_id, int $question_id): string
1677
    {
1678
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1679
        $sql = "SELECT teacher_comment
1680
                FROM $table
1681
                WHERE
1682
                    exe_id = $exe_id AND
1683
                    question_id = $question_id
1684
                ORDER by question_id";
1685
        $sqlResult = Database::query($sql);
1686
        $comm = strval(Database::result($sqlResult, 0, 'teacher_comment'));
1687
1688
        return trim($comm);
1689
    }
1690
1691
    /**
1692
     * Get all the track_e_attempt records for a given
1693
     * track_e_exercises.exe_id (pk).
1694
     *
1695
     * @param int $exeId The exe_id from an exercise attempt record
1696
     *
1697
     * @return array The complete records from track_e_attempt that match the given exe_id
1698
     * @throws \Doctrine\DBAL\Exception
1699
     * @throws Exception
1700
     */
1701
    public static function getAllExerciseEventByExeId(int $exeId): array
1702
    {
1703
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1704
1705
        $sql = "SELECT * FROM $table
1706
                WHERE exe_id = $exeId
1707
                ORDER BY position";
1708
        $res_question = Database::query($sql);
1709
        $list = [];
1710
        if (Database::num_rows($res_question)) {
1711
            while ($row = Database::fetch_assoc($res_question)) {
1712
                $list[$row['question_id']][] = $row;
1713
            }
1714
        }
1715
1716
        return $list;
1717
    }
1718
1719
    /**
1720
     * Get a question attempt from track_e_attempt based on en exe_id and question_id
1721
     * @param int $exeId
1722
     * @param int $questionId
1723
     * @return array
1724
     * @throws \Doctrine\DBAL\Exception
1725
     * @throws Exception
1726
     */
1727
    public static function getQuestionAttemptByExeIdAndQuestion(int $exeId, int $questionId): array
1728
    {
1729
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1730
1731
        $sql = "SELECT * FROM $table
1732
                WHERE
1733
                    exe_id = $exeId AND
1734
                    question_id = $questionId
1735
                ORDER BY position";
1736
        $result = Database::query($sql);
1737
        $attempt = [];
1738
        if (Database::num_rows($result)) {
1739
            $attempt = Database::fetch_assoc($result);
1740
        }
1741
1742
        return $attempt;
1743
    }
1744
1745
    /**
1746
     * Delete one record from the track_e_attempt table (recorded quiz answer)
1747
     * and register the deletion event (LOG_QUESTION_RESULT_DELETE) in
1748
     * track_e_default.
1749
     *
1750
     * @param int $exeId The track_e_exercises.exe_id (primary key)
1751
     * @param int $user_id The user who answered (already contained in exe_id)
1752
     * @param int $courseId The course in which it happened (already contained in exe_id)
1753
     * @param int $session_id The session in which it happened (already contained in exe_id)
1754
     * @param int $question_id The c_quiz_question.iid
1755
     * @throws ORMException
1756
     * @throws Exception
1757
     */
1758
    public static function delete_attempt(
1759
        int $exeId,
1760
        int $user_id,
1761
        int $courseId,
1762
        int $session_id,
1763
        int $question_id
1764
    ): void
1765
    {
1766
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1767
1768
        $sql = "DELETE FROM $table
1769
                WHERE
1770
                    exe_id = $exeId AND
1771
                    user_id = $user_id AND
1772
                    question_id = $question_id ";
1773
        Database::query($sql);
1774
1775
        self::addEvent(
1776
            LOG_QUESTION_RESULT_DELETE,
1777
            LOG_EXERCISE_ATTEMPT_QUESTION_ID,
1778
            $exeId.'-'.$question_id,
1779
            null,
1780
            null,
1781
            $courseId,
1782
            $session_id
1783
        );
1784
    }
1785
1786
    /**
1787
     * Delete one record from the track_e_hotspot table based on a given
1788
     * track_e_exercises.exe_id.
1789
     *
1790
     * @param int $exeId
1791
     * @param int $user_id
1792
     * @param int $courseId
1793
     * @param int $question_id
1794
     * @param ?int $sessionId
1795
     * @throws ORMException
1796
     * @throws Exception
1797
     */
1798
    public static function delete_attempt_hotspot(
1799
        int $exeId,
1800
        int $user_id,
1801
        int $courseId,
1802
        int $question_id,
1803
        ?int $sessionId = null
1804
    ): void
1805
    {
1806
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
1807
1808
        if (!isset($sessionId)) {
1809
            $sessionId = api_get_session_id();
1810
        }
1811
1812
        $sql = "DELETE FROM $table
1813
                WHERE
1814
                    hotspot_exe_id = $exeId AND
1815
                    hotspot_user_id = $user_id AND
1816
                    c_id = $courseId AND
1817
                    hotspot_question_id = $question_id ";
1818
        Database::query($sql);
1819
        self::addEvent(
1820
            LOG_QUESTION_RESULT_DELETE,
1821
            LOG_EXERCISE_ATTEMPT_QUESTION_ID,
1822
            $exeId.'-'.$question_id,
1823
            null,
1824
            null,
1825
            $courseId,
1826
            $sessionId
1827
        );
1828
    }
1829
1830
    /**
1831
     * Registers in track_e_course_access when user logs in for the first time to a course.
1832
     *
1833
     * @param int $courseId ID of the course
1834
     * @param int $user_id ID of the user
1835
     * @param int $sessionId ID of the session (if any)
1836
     *
1837
     * @return bool
1838
     * @throws Exception
1839
     */
1840
    public static function eventCourseLogin(int $courseId, int $user_id, int $sessionId): bool
1841
    {
1842
        if (Session::read('login_as')) {
1843
            return false;
1844
        }
1845
1846
        if (false === self::isSessionLogNeedToBeSave($sessionId)) {
1847
            return false;
1848
        }
1849
1850
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
1851
        $loginDate = $logoutDate = api_get_utc_datetime();
1852
1853
        // $counter represents the number of time this record has been refreshed
1854
        $counter = 1;
1855
        $ip = Database::escape_string(api_get_real_ip());
1856
1857
        $sql = "INSERT INTO $table(c_id, user_ip, user_id, login_course_date, logout_course_date, counter, session_id)
1858
                VALUES($courseId, '$ip', $user_id, '$loginDate', '$logoutDate', $counter, $sessionId)";
1859
        $courseAccessId = Database::query($sql);
1860
1861
        if ($courseAccessId) {
1862
            // Course catalog stats modifications see #4191
1863
            CourseManager::update_course_ranking(
1864
                null,
1865
                0,
1866
                null,
1867
                null,
1868
                true,
1869
                false
1870
            );
1871
1872
            return true;
1873
        }
1874
1875
        return false;
1876
    }
1877
1878
    /**
1879
     * Updates the user - course - session every X minutes
1880
     *
1881
     * @param int $courseId
1882
     * @param int $userId
1883
     * @param int $sessionId
1884
     * @param ?int $minutes
1885
     *
1886
     * @return bool
1887
     * @throws \Doctrine\DBAL\Exception
1888
     * @throws Exception
1889
     */
1890
    public static function eventCourseLoginUpdate(
1891
        int $courseId,
1892
        int $userId,
1893
        int $sessionId,
1894
        ?int $minutes = 5
1895
    ): bool
1896
    {
1897
        if (Session::read('login_as')) {
1898
            return false;
1899
        }
1900
1901
        if (empty($courseId) || empty($userId)) {
1902
            return false;
1903
        }
1904
1905
        if (false === self::isSessionLogNeedToBeSave($sessionId)) {
1906
            return false;
1907
        }
1908
1909
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
1910
        $sql = "SELECT course_access_id, logout_course_date
1911
                FROM $table
1912
                WHERE
1913
                    c_id = $courseId AND
1914
                    session_id = $sessionId AND
1915
                    user_id = $userId
1916
                ORDER BY login_course_date DESC
1917
                LIMIT 1";
1918
1919
        $result = Database::query($sql);
1920
1921
        // Save every 5 minutes by default
1922
        $seconds = $minutes * 60;
1923
        $maxSeconds = 3600; // Only update if max diff is one hour
1924
        if (Database::num_rows($result)) {
1925
            $row = Database::fetch_array($result);
1926
            $id = $row['course_access_id'];
1927
            $logout = $row['logout_course_date'];
1928
            $now = time();
1929
            $logout = api_strtotime($logout, 'UTC');
1930
            if ($now - $logout > $seconds &&
1931
                $now - $logout < $maxSeconds
1932
            ) {
1933
                $now = api_get_utc_datetime();
1934
                $sql = "UPDATE $table SET
1935
                            logout_course_date = '$now',
1936
                            counter = counter + 1
1937
                        WHERE course_access_id = $id";
1938
                Database::query($sql);
1939
            }
1940
1941
            return true;
1942
        }
1943
1944
        return false;
1945
    }
1946
1947
    /**
1948
     * Register the logout of the course (usually when logging out of the platform)
1949
     * from the track_e_course_access table.
1950
     *
1951
     * @param array $logoutInfo Information stored by local.inc.php
1952
     *                          before new context ['uid'=> x, 'cid'=>y, 'sid'=>z]
1953
     *
1954
     * @return bool
1955
     * @throws \Doctrine\DBAL\Exception
1956
     * @throws Exception
1957
     */
1958
    public static function courseLogout(array $logoutInfo): bool
1959
    {
1960
        if (Session::read('login_as')) {
1961
            return false;
1962
        }
1963
1964
        if (empty($logoutInfo['uid']) || empty($logoutInfo['cid'])) {
1965
            return false;
1966
        }
1967
1968
        $sessionLifetime = api_get_configuration_value('session_lifetime');
1969
        /*
1970
         * When $_configuration['session_lifetime'] is larger than ~100 hours
1971
         * (in order to let users take exercises with no problems)
1972
         * the function Tracking::get_time_spent_on_the_course() returns larger values (200h) due the condition:
1973
         * login_course_date > now() - INTERVAL $session_lifetime SECOND
1974
         */
1975
        if (empty($sessionLifetime) || $sessionLifetime > 86400) {
1976
            $sessionLifetime = 3600; // 1 hour
1977
        }
1978
        if (!empty($logoutInfo) && !empty($logoutInfo['cid'])) {
1979
            $sessionId = 0;
1980
            if (!empty($logoutInfo['sid'])) {
1981
                $sessionId = (int) $logoutInfo['sid'];
1982
            }
1983
1984
            if (false === self::isSessionLogNeedToBeSave($sessionId)) {
1985
                return false;
1986
            }
1987
1988
            $tableCourseAccess = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
1989
            $userId = (int) $logoutInfo['uid'];
1990
            $courseId = (int) $logoutInfo['cid'];
1991
1992
            $currentDate = api_get_utc_datetime();
1993
            // UTC time
1994
            $diff = time() - $sessionLifetime;
1995
            $time = api_get_utc_datetime($diff);
1996
            $sql = "SELECT course_access_id, logout_course_date
1997
                    FROM $tableCourseAccess
1998
                    WHERE
1999
                        user_id = $userId AND
2000
                        c_id = $courseId  AND
2001
                        session_id = $sessionId AND
2002
                        login_course_date > '$time'
2003
                    ORDER BY login_course_date DESC
2004
                    LIMIT 1";
2005
            $result = Database::query($sql);
2006
            $insert = false;
2007
            if (Database::num_rows($result) > 0) {
2008
                $row = Database::fetch_assoc($result);
2009
                $courseAccessId = $row['course_access_id'];
2010
                $sql = "UPDATE $tableCourseAccess SET
2011
                                logout_course_date = '$currentDate',
2012
                                counter = counter + 1
2013
                            WHERE course_access_id = $courseAccessId";
2014
                Database::query($sql);
2015
            } else {
2016
                $insert = true;
2017
            }
2018
2019
            if ($insert) {
2020
                $ip = Database::escape_string(api_get_real_ip());
2021
                $sql = "INSERT INTO $tableCourseAccess (c_id, user_ip, user_id, login_course_date, logout_course_date, counter, session_id)
2022
                        VALUES ($courseId, '$ip', $userId, '$currentDate', '$currentDate', 1, $sessionId)";
2023
                Database::query($sql);
2024
            }
2025
2026
            return true;
2027
        }
2028
2029
        return false;
2030
    }
2031
2032
    /**
2033
     * Register a "fake" time spent on the platform, for example to match the
2034
     * estimated time he took to author an assignment/work, see configuration
2035
     * setting considered_working_time.
2036
     * This assumes there is already some connection of the student to the
2037
     * course, otherwise he wouldn't be able to upload an assignment.
2038
     * This works by creating a new record, copy of the current one, then
2039
     * updating the current one to be just the considered_working_time and
2040
     * end at the same second as the user connected to the course.
2041
     *
2042
     * @param int    $courseId The course in which to add the time
2043
     * @param int    $userId The user for whom to add the time
2044
     * @param int    $sessionId The session in which to add the time (if any)
2045
     * @param string $virtualTime The amount of time to be added,
2046
     *                            in a hh:mm:ss format. If int, we consider it is expressed in hours.
2047
     * @param int    $workId Student publication id result
2048
     *
2049
     * @return bool true on successful insertion, false otherwise
2050
     * @throws \Doctrine\DBAL\Exception
2051
     * @throws Exception
2052
     */
2053
    public static function eventAddVirtualCourseTime(
2054
        int $courseId,
2055
        int $userId,
2056
        int $sessionId,
2057
        string $virtualTime,
2058
        int $workId
2059
    ): bool
2060
    {
2061
        if (empty($virtualTime)) {
2062
            return false;
2063
        }
2064
2065
        $logoutDate = api_get_utc_datetime();
2066
        $loginDate = ChamiloApi::addOrSubTimeToDateTime(
2067
            $virtualTime,
2068
            $logoutDate,
2069
            false
2070
        );
2071
2072
        $ip = api_get_real_ip();
2073
        $params = [
2074
            'login_course_date' => $loginDate,
2075
            'logout_course_date' => $logoutDate,
2076
            'session_id' => $sessionId,
2077
            'user_id' => $userId,
2078
            'counter' => 0,
2079
            'c_id' => $courseId,
2080
            'user_ip' => $ip,
2081
        ];
2082
        $courseTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
2083
        Database::insert($courseTrackingTable, $params);
2084
2085
        // Time should also be added to the track_e_login table to
2086
        // affect total time on the platform
2087
        $params = [
2088
            'login_user_id' => $userId,
2089
            'login_date' => $loginDate,
2090
            'user_ip' => $ip,
2091
            'logout_date' => $logoutDate,
2092
        ];
2093
        $platformTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
2094
        Database::insert($platformTrackingTable, $params);
2095
2096
        if (Tracking::minimumTimeAvailable($sessionId, $courseId)) {
2097
            $uniqueId = time();
2098
            $logInfo = [
2099
                'c_id' => $courseId,
2100
                'session_id' => $sessionId,
2101
                'tool' => TOOL_STUDENTPUBLICATION,
2102
                'date_reg' => $loginDate,
2103
                'action' => 'add_work_start_'.$workId,
2104
                'action_details' => $virtualTime,
2105
                'user_id' => $userId,
2106
                'current_id' => $uniqueId,
2107
            ];
2108
            self::registerLog($logInfo);
2109
2110
            $logInfo = [
2111
                'c_id' => $courseId,
2112
                'session_id' => $sessionId,
2113
                'tool' => TOOL_STUDENTPUBLICATION,
2114
                'date_reg' => $logoutDate,
2115
                'action' => 'add_work_end_'.$workId,
2116
                'action_details' => $virtualTime,
2117
                'user_id' => $userId,
2118
                'current_id' => $uniqueId,
2119
            ];
2120
            self::registerLog($logInfo);
2121
        }
2122
2123
        return true;
2124
    }
2125
2126
    /**
2127
     * Removes a "fake" time spent on the platform, for example to match the
2128
     * estimated time he took to author an assignment/work, see configuration
2129
     * setting considered_working_time.
2130
     * This method should be called when something that generated a fake
2131
     * time record is removed. Given the database link is weak (no real
2132
     * relationship kept between the deleted item and this record), this
2133
     * method just looks for the latest record that has the same time as the
2134
     * item's fake time, is in the past and in this course+session. If such a
2135
     * record cannot be found, it doesn't do anything.
2136
     * The IP address is not considered a useful filter here.
2137
     *
2138
     * @param int    $courseId The course in which to add the time
2139
     * @param int    $userId The user for whom to add the time
2140
     * @param int    $sessionId The session in which to add the time (if any)
2141
     * @param string $virtualTime The amount of time to be added, in a hh:mm:ss format. If int, we consider it is
2142
     *                            expressed in hours.
2143
     * @param int $workId
2144
     *
2145
     * @return bool true on successful removal, false otherwise
2146
     * @throws \Doctrine\DBAL\Exception
2147
     * @throws Exception
2148
     */
2149
    public static function eventRemoveVirtualCourseTime(
2150
        int $courseId,
2151
        int $userId,
2152
        int $sessionId,
2153
        string $virtualTime,
2154
        int $workId
2155
    ):bool
2156
    {
2157
        if (empty($virtualTime)) {
2158
            return false;
2159
        }
2160
2161
        $originalVirtualTime = Database::escape_string($virtualTime);
2162
2163
        $courseTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
2164
        $platformTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
2165
2166
        // Change $virtualTime format from hh:mm:ss to hhmmss which is the
2167
        // format returned by SQL for a subtraction of two datetime values
2168
        // @todo make sure this is portable between DBMSes
2169
        // @todo make sure this is portable between DBMSes
2170
        if (preg_match('/:/', $virtualTime)) {
2171
            [$h, $m, $s] = preg_split('/:/', $virtualTime);
2172
            $virtualTime = (int) $h * 3600 + (int) $m * 60 + (int) $s;
2173
        } else {
2174
            $virtualTime = (int) $virtualTime * 3600;
2175
        }
2176
2177
        // Get the current latest course connection register. We need that
2178
        // record to re-use the data and create a new record.
2179
        $sql = "SELECT course_access_id
2180
                FROM $courseTrackingTable
2181
                WHERE
2182
                    user_id = $userId AND
2183
                    c_id = $courseId  AND
2184
                    session_id  = $sessionId AND
2185
                    counter = 0 AND
2186
                    (UNIX_TIMESTAMP(logout_course_date) - UNIX_TIMESTAMP(login_course_date)) = '$virtualTime'
2187
                ORDER BY login_course_date DESC LIMIT 0,1";
2188
        $result = Database::query($sql);
2189
2190
        // Ignore if we didn't find any course connection record in the last
2191
        // hour. In this case it wouldn't be right to add a "fake" time record.
2192
        if (Database::num_rows($result) > 0) {
2193
            // Found the latest connection
2194
            $row = Database::fetch_row($result);
2195
            $courseAccessId = $row[0];
2196
            $sql = "DELETE FROM $courseTrackingTable
2197
                    WHERE course_access_id = $courseAccessId";
2198
            Database::query($sql);
2199
        }
2200
        $sql = "SELECT login_id
2201
                FROM $platformTrackingTable
2202
                WHERE
2203
                    login_user_id = $userId AND
2204
                    (UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date)) = '$virtualTime'
2205
                ORDER BY login_date DESC LIMIT 0,1";
2206
        $result = Database::query($sql);
2207
        if (Database::num_rows($result) > 0) {
2208
            $row = Database::fetch_row($result);
2209
            $loginAccessId = $row[0];
2210
            $sql = "DELETE FROM $platformTrackingTable
2211
                    WHERE login_id = $loginAccessId";
2212
            Database::query($sql);
2213
        }
2214
2215
        if (Tracking::minimumTimeAvailable($sessionId, $courseId)) {
2216
            $sql = "SELECT id FROM track_e_access_complete
2217
                    WHERE
2218
                        tool = '".TOOL_STUDENTPUBLICATION."' AND
2219
                        c_id = $courseId AND
2220
                        session_id = $sessionId AND
2221
                        user_id = $userId AND
2222
                        action_details = '$originalVirtualTime' AND
2223
                        action = 'add_work_start_$workId' ";
2224
            $result = Database::query($sql);
2225
            $result = Database::fetch_array($result);
2226
            if ($result) {
2227
                $sql = 'DELETE FROM track_e_access_complete WHERE id = '.$result['id'];
2228
                Database::query($sql);
2229
            }
2230
2231
            $sql = "SELECT id FROM track_e_access_complete
2232
                    WHERE
2233
                        tool = '".TOOL_STUDENTPUBLICATION."' AND
2234
                        c_id = $courseId AND
2235
                        session_id = $sessionId AND
2236
                        user_id = $userId AND
2237
                        action_details = '$originalVirtualTime' AND
2238
                        action = 'add_work_end_$workId' ";
2239
            $result = Database::query($sql);
2240
            $result = Database::fetch_array($result);
2241
            if ($result) {
2242
                $sql = 'DELETE FROM track_e_access_complete WHERE id = '.$result['id'];
2243
                Database::query($sql);
2244
            }
2245
        }
2246
2247
        return false;
2248
    }
2249
2250
    /**
2251
     * Register the logout of the course (usually when logging out of the platform)
2252
     * from the track_e_access_complete table.
2253
     *
2254
     * @param array $logInfo Information stored by local.inc.php
2255
     *
2256
     * @return bool
2257
     * @throws \Doctrine\DBAL\Exception
2258
     */
2259
    public static function registerLog(array $logInfo): bool
2260
    {
2261
        $sessionId = api_get_session_id();
2262
        $courseId = api_get_course_int_id();
2263
2264
        if (isset($logInfo['c_id']) && !empty($logInfo['c_id'])) {
2265
            $courseId = $logInfo['c_id'];
2266
        }
2267
2268
        if (isset($logInfo['session_id']) && !empty($logInfo['session_id'])) {
2269
            $sessionId = $logInfo['session_id'];
2270
        }
2271
2272
        if (!Tracking::minimumTimeAvailable($sessionId, $courseId)) {
2273
            return false;
2274
        }
2275
2276
        if (false === self::isSessionLogNeedToBeSave($sessionId)) {
2277
            return false;
2278
        }
2279
2280
        $loginAs = true === (int) Session::read('login_as');
2281
2282
        $logInfo['user_id'] = isset($logInfo['user_id']) ? $logInfo['user_id'] : api_get_user_id();
2283
        $logInfo['date_reg'] = isset($logInfo['date_reg']) ? $logInfo['date_reg'] : api_get_utc_datetime();
2284
        $logInfo['tool'] = !empty($logInfo['tool']) ? $logInfo['tool'] : '';
2285
        $logInfo['tool_id'] = !empty($logInfo['tool_id']) ? (int) $logInfo['tool_id'] : 0;
2286
        $logInfo['tool_id_detail'] = !empty($logInfo['tool_id_detail']) ? (int) $logInfo['tool_id_detail'] : 0;
2287
        $logInfo['action'] = !empty($logInfo['action']) ? $logInfo['action'] : '';
2288
        $logInfo['action_details'] = !empty($logInfo['action_details']) ? $logInfo['action_details'] : '';
2289
        $logInfo['ip_user'] = api_get_real_ip();
2290
        $logInfo['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
2291
        $logInfo['session_id'] = $sessionId;
2292
        $logInfo['c_id'] = $courseId;
2293
        $logInfo['ch_sid'] = session_id();
2294
        $logInfo['login_as'] = $loginAs;
2295
        $logInfo['info'] = !empty($logInfo['info']) ? $logInfo['info'] : '';
2296
        $logInfo['url'] = $_SERVER['REQUEST_URI'];
2297
        $logInfo['current_id'] = isset($logInfo['current_id']) ? $logInfo['current_id'] : Session::read('last_id', 0);
2298
2299
        $id = Database::insert('track_e_access_complete', $logInfo);
2300
        if ($id && empty($logInfo['current_id'])) {
2301
            Session::write('last_id', $id);
2302
        }
2303
2304
        return true;
2305
    }
2306
2307
    /**
2308
     * Get the remaining time to answer a question when there is question-based timing in place ('time' field exists for question items)
2309
     * @param int $exeId
2310
     * @param int $questionId
2311
     * @return int
2312
     * @throws \Doctrine\DBAL\Exception
2313
     */
2314
    public static function getAttemptQuestionDuration(int $exeId, int $questionId): int
2315
    {
2316
        // Check current attempt.
2317
        $questionAttempt = self::getQuestionAttemptByExeIdAndQuestion($exeId, $questionId);
2318
        $alreadySpent = 0;
2319
        if (!empty($questionAttempt) && $questionAttempt['seconds_spent']) {
2320
            $alreadySpent = $questionAttempt['seconds_spent'];
2321
        }
2322
        $now = time();
2323
        $questionStart = Session::read('question_start', []);
2324
        if (!empty($questionStart) &&
2325
            !empty($questionStart[$questionId])
2326
        ) {
2327
            $time = $questionStart[$questionId];
2328
        } else {
2329
            $diff = 0;
2330
            if (!empty($alreadySpent)) {
2331
                $diff = $alreadySpent;
2332
            }
2333
            $time = $questionStart[$questionId] = $now - $diff;
2334
            Session::write('question_start', $questionStart);
2335
        }
2336
2337
        return $now - $time;
2338
    }
2339
2340
    /**
2341
     * Wrapper to addEvent with event LOG_SUBSCRIBE_USER_TO_COURSE
2342
     * @param User         $subscribedUser
2343
     * @param CourseEntity $course
2344
     * @return void
2345
     * @throws ORMException
2346
     */
2347
    public static function logSubscribedUserInCourse(
2348
        User $subscribedUser,
2349
        CourseEntity $course
2350
    ): void
2351
    {
2352
        $dateTime = api_get_utc_datetime();
2353
        $registrantId = api_get_user_id();
2354
2355
        self::addEvent(
2356
            LOG_SUBSCRIBE_USER_TO_COURSE,
2357
            LOG_COURSE_CODE,
2358
            $course->getCode(),
2359
            $dateTime,
2360
            $registrantId,
2361
            $course->getId()
2362
        );
2363
2364
        self::addEvent(
2365
            LOG_SUBSCRIBE_USER_TO_COURSE,
2366
            LOG_USER_OBJECT,
2367
            api_get_user_info($subscribedUser->getId()),
2368
            $dateTime,
2369
            $registrantId,
2370
            $course->getId()
2371
        );
2372
    }
2373
2374
    /**
2375
     * Wrapper to addEvent with event LOG_SESSION_ADD_USER_COURSE and LOG_SUBSCRIBE_USER_TO_COURSE
2376
     * @param User          $userSubscribed
2377
     * @param CourseEntity  $course
2378
     * @param SessionEntity $session
2379
     * @return void
2380
     * @throws ORMException
2381
     */
2382
    public static function logUserSubscribedInCourseSession(
2383
        User $userSubscribed,
2384
        CourseEntity $course,
2385
        SessionEntity $session
2386
    ): void
2387
    {
2388
        $dateTime = api_get_utc_datetime();
2389
        $registrantId = api_get_user_id();
2390
2391
        self::addEvent(
2392
            LOG_SESSION_ADD_USER_COURSE,
2393
            LOG_USER_ID,
2394
            $userSubscribed,
2395
            $dateTime,
2396
            $registrantId,
2397
            $course->getId(),
2398
            $session->getId()
2399
        );
2400
        self::addEvent(
2401
            LOG_SUBSCRIBE_USER_TO_COURSE,
2402
            LOG_COURSE_CODE,
2403
            $course->getCode(),
2404
            $dateTime,
2405
            $registrantId,
2406
            $course->getId(),
2407
            $session->getId()
2408
        );
2409
        self::addEvent(
2410
            LOG_SUBSCRIBE_USER_TO_COURSE,
2411
            LOG_USER_OBJECT,
2412
            api_get_user_info($userSubscribed->getId()),
2413
            $dateTime,
2414
            $registrantId,
2415
            $course->getId(),
2416
            $session->getId()
2417
        );
2418
    }
2419
}
2420