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

Event::updateEventExercise()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 65
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 37
nc 17
nop 13
dl 0
loc 65
rs 8.7057
c 0
b 0
f 0

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