Event::saveQuestionAttempt()   F
last analyzed

Complexity

Conditions 22
Paths > 20000

Size

Total Lines 171
Code Lines 103

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 22
eloc 103
nop 15
dl 0
loc 171
rs 0
c 0
b 0
f 0
nc 28944

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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