Passed
Push — master ( 21a798...446031 )
by Yannick
11:04
created

Event::get_all_exercise_results_by_user()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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