Exercise   F
last analyzed

Complexity

Total Complexity 1517

Size/Duplication

Total Lines 11409
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6207
c 1
b 0
f 0
dl 0
loc 11409
rs 0.8
wmc 1517

187 Methods

Rating   Name   Duplication   Size   Complexity  
A getCutTitle() 0 5 1
A updateAttempts() 0 3 1
A setRandom() 0 3 1
A updateCategories() 0 5 2
A updateResultsDisabled() 0 3 1
A enable_results() 0 3 1
A updateTitle() 0 3 1
A updateDescription() 0 3 1
A enable() 0 3 1
A updateType() 0 3 1
A updatePassPercentage() 0 3 1
A updateExpiredTime() 0 3 1
A updateRandomAnswers() 0 3 1
A disable() 0 3 1
A setModelType() 0 3 1
B updateSound() 0 29 8
A updateEndButton() 0 3 1
A setNotifyUserByEmail() 0 3 1
A getQuestionSelectionType() 0 3 1
A updateEmailNotificationTemplate() 0 3 1
A updatePropagateNegative() 0 3 1
A updateFeedbackType() 0 3 1
A setEmailNotificationTemplateToUser() 0 3 1
A setOnSuccessMessage() 0 3 1
A setOnFailedMessage() 0 3 1
A disable_results() 0 3 1
A updateSaveCorrectAnswers() 0 3 1
A setQuestionSelectionType() 0 3 1
A updateReviewAnswers() 0 3 3
A getId() 0 3 1
A isInList() 0 8 2
A addToList() 0 18 4
A removeFromList() 0 20 6
A update_question_positions() 0 24 4
B selectQuestionList() 0 38 7
A getRandomByCategory() 0 3 1
A getSound() 0 3 1
A getHideQuestionTitle() 0 3 1
A hasQuestionWithTypeNotInList() 0 22 2
A selectResultsDisabled() 0 3 1
A getModelType() 0 3 1
A setGlobalCategoryId() 0 6 3
A setHideQuestionTitle() 0 3 1
A updateDisplayCategoryName() 0 3 1
A getRandomList() 0 34 6
A getFeedbackType() 0 3 1
A hasQuestion() 0 17 1
A setScoreTypeModel() 0 3 1
A selectTimeLimit() 0 3 1
A getTextWhenFinishedFailure() 0 7 2
F getQuestionListWithCategoryListFilteredByCategorySettings() 0 233 20
A selectDescription() 0 3 1
A hasQuestionWithType() 0 17 1
A isRandomByCat() 0 10 3
A setCategoriesGrouping() 0 3 1
A selectEndButton() 0 3 1
A selectTitle() 0 7 2
A getGlobalCategoryId() 0 3 1
A updateRandomByCat() 0 12 2
A selectPropagateNeg() 0 3 1
A getRandomAnswers() 0 3 1
A selectNbrQuestions() 0 3 1
A getScoreTypeModel() 0 3 1
A selectDisplayCategoryName() 0 3 1
A selectType() 0 3 1
A getQuestionOrderedListByName() 0 24 4
A selectStatus() 0 3 1
A getShuffle() 0 3 1
A setTextWhenFinishedFailure() 0 3 1
A getSaveCorrectAnswers() 0 3 1
A getTextWhenFinished() 0 3 1
A getQuestionCount() 0 19 2
A selectPassPercentage() 0 3 1
A setTextWhenFinished() 0 3 1
C getQuestionListPagination() 0 106 16
A selectAttempts() 0 3 1
A isRandom() 0 9 3
B search_engine_edit() 0 73 9
A save_stat_track_exercise_info() 0 51 3
F read() 0 108 22
D processCreation() 0 100 11
A __construct() 0 52 2
F show_button() 0 177 37
B setResultFeedbackGroup() 0 93 6
B cleanResults() 0 70 7
B delete() 0 68 9
A updateStatus() 0 3 1
B copyExercise() 0 53 11
B search_engine_save() 0 52 8
A selectExpiredTime() 0 3 1
F createForm() 0 652 40
F save() 0 130 21
B search_engine_delete() 0 38 6
A get_stat_track_exercise_info() 0 31 2
A countQuestionsInExercise() 0 15 5
A mediaIsActivated() 0 18 6
A saveCategoriesInExercise() 0 15 5
A getQuestionForTeacher() 0 28 3
C getDelineationResult() 0 131 12
C transformQuestionListWithMedias() 0 44 12
C progressExercisePaginationBarWithCategories() 0 120 15
A setPageResultConfigurationDefaults() 0 5 3
A cleanCourseLaunchSettings() 0 18 2
C renderQuestionList() 0 113 14
D renderQuestion() 0 181 19
A addAllQuestionToRemind() 0 17 2
A getPreventBackwards() 0 3 1
A getResultsAccess() 0 12 2
A countUserAnswersSavedInExercise() 0 5 1
B getQuestionRibbon() 0 38 7
A showTimeControlJS() 0 80 4
A fill_in_blank_answer_to_array() 0 10 2
A getMediaList() 0 3 1
A getCorrectAnswersInAllAttempts() 0 3 1
A format_title_variable() 0 3 1
A enableAutoLaunch() 0 6 1
A setQuizCategoryId() 0 4 2
F manage_answer() 0 3027 514
B get_validated_question_list() 0 62 8
B progressExercisePaginationBar() 0 58 7
B get_exercise_result() 0 37 6
A get_stat_track_exercise_info_by_exe_id() 0 19 5
A setNotifications() 0 3 1
A getQuizCategoryId() 0 7 2
C pickQuestionsPerCategory() 0 71 16
A getUserAnswersSavedInExerciseStatic() 0 8 3
B getUserAnswersSavedInExercise() 0 31 8
A getAutoLaunch() 0 3 1
A getQuestionWithCategories() 0 24 2
A setHideQuestionNumber() 0 3 1
B sendNotificationForOralQuestions() 0 81 11
A getResultAccess() 0 13 3
A getCategoriesInExercise() 0 18 4
A get_question_list() 0 6 1
D generateStats() 0 135 19
B allowAction() 0 36 9
F exerciseGridResource() 0 765 124
A getFinishText() 0 18 4
C editQuestionToRemind() 0 52 13
A setPageResultConfiguration() 0 12 2
B getReminderTable() 0 133 10
A getHideQuestionNumber() 0 3 1
F is_visible() 0 243 44
C getNextQuestionId() 0 70 16
A getQuestionListWithMediasCompressed() 0 3 1
A cleanSessionVariables() 0 20 1
A getResultAccessTimeDiff() 0 13 3
A getQuestionOrderedList() 0 56 5
A getPageResultConfiguration() 0 8 2
A setShowPreviousButton() 0 5 1
A showSimpleTimeControl() 0 63 1
A fill_in_blank_answer_to_string() 0 19 4
C showExerciseResultHeader() 0 112 13
A added_in_lp() 0 13 2
A getNotifications() 0 3 1
A getExercisesByCourseSession() 0 21 2
A getExerciseAndResult() 0 40 5
A getQuestionListWithMediasUncompressed() 0 3 1
A getPositionInCompressedQuestionList() 0 23 6
A setMediaList() 0 19 1
A getLpListFromExercise() 0 25 2
A removeAllQuestionToRemind() 0 13 2
B isBlockedByPercentage() 0 36 8
A getUnformattedTitle() 0 3 1
B getRadarsFromUsers() 0 52 8
A getQuestionList() 0 3 1
A getPageConfigurationAttribute() 0 9 2
B saveExerciseInLp() 0 80 10
A get_formated_title_variable() 0 3 1
C transform_question_list_with_medias() 0 42 12
B getMaxScore() 0 41 9
C sendNotificationForOpenQuestions() 0 93 11
A showExpectedChoiceColumn() 0 24 5
C getAverageRadarsFromUsers() 0 69 13
A setQuestionList() 0 9 1
C getAnswersInAllAttempts() 0 59 15
F send_mail_notification_for_exam() 0 193 31
A format_title() 0 3 1
A showPreviousButton() 0 8 2
A get_formated_title() 0 6 2
A getNumberQuestionExerciseCategory() 0 16 3
A showExpectedChoice() 0 3 1
B setResultDisabledGroup() 0 133 2
A hasResultsAccess() 0 8 2
A getLpBySession() 0 18 5
B getRadar() 0 73 7
A returnTimeLeftDiv() 0 20 1

How to fix   Complexity   

Complex Class

Complex classes like Exercise often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Exercise, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\GradebookLink;
6
use Chamilo\CoreBundle\Entity\ResourceLink;
7
use Chamilo\CoreBundle\Entity\TrackEExercise;
8
use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
9
use Chamilo\CoreBundle\Entity\TrackEHotspot;
10
use Chamilo\CoreBundle\Enums\ActionIcon;
11
use Chamilo\CoreBundle\Enums\StateIcon;
12
use Chamilo\CoreBundle\Enums\ToolIcon;
13
use Chamilo\CoreBundle\Framework\Container;
14
use Chamilo\CoreBundle\Repository\ResourceLinkRepository;
15
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
16
use Chamilo\CoreBundle\Helpers\ChamiloHelper;
17
use Chamilo\CourseBundle\Entity\CQuiz;
18
use Chamilo\CourseBundle\Entity\CQuizCategory;
19
use Chamilo\CourseBundle\Entity\CQuizRelQuestionCategory;
20
use ChamiloSession as Session;
21
22
23
/**
24
 * @author Olivier Brouckaert
25
 * @author Julio Montoya Cleaning exercises
26
 * @author Hubert Borderiou #294
27
 */
28
class Exercise
29
{
30
    public const PAGINATION_ITEMS_PER_PAGE = 20;
31
    public $iId;
32
    public $id;
33
    public $name;
34
    public $title;
35
    public $exercise;
36
    public $description;
37
    public $sound;
38
    public $type; //ALL_ON_ONE_PAGE or ONE_PER_PAGE
39
    public $random;
40
    public $random_answers;
41
    public $active;
42
    public $timeLimit;
43
    public $attempts;
44
    public $feedback_type;
45
    public $end_time;
46
    public $start_time;
47
    public $questionList; // array with the list of this exercise's questions
48
    /* including question list of the media */
49
    public $questionListUncompressed;
50
    public $results_disabled;
51
    public $expired_time;
52
    public $course;
53
    public $course_id;
54
    public $propagate_neg;
55
    public $saveCorrectAnswers;
56
    public $review_answers;
57
    public $randomByCat;
58
    public $text_when_finished;
59
    public $text_when_finished_failure;
60
    public $display_category_name;
61
    public $pass_percentage;
62
    public $edit_exercise_in_lp = false;
63
    public $is_gradebook_locked = false;
64
    public $exercise_was_added_in_lp = false;
65
    public $lpList = [];
66
    public $force_edit_exercise_in_lp = false;
67
    public $categories;
68
    public $categories_grouping = true;
69
    public $endButton = 0;
70
    public $categoryWithQuestionList;
71
    public $mediaList;
72
    public $loadQuestionAJAX = false;
73
    // Notification send to the teacher.
74
    public $emailNotificationTemplate = null;
75
    // Notification send to the student.
76
    public $emailNotificationTemplateToUser = null;
77
    public $countQuestions = 0;
78
    public $fastEdition = false;
79
    public $modelType = 1;
80
    public $questionSelectionType = EX_Q_SELECTION_ORDERED;
81
    public $hideQuestionTitle = 0;
82
    public $scoreTypeModel = 0;
83
    public $categoryMinusOne = true; // Shows the category -1: See BT#6540
84
    public $globalCategoryId = null;
85
    public $onSuccessMessage = null;
86
    public $onFailedMessage = null;
87
    public $emailAlert;
88
    public $notifyUserByEmail = '';
89
    public $sessionId = 0;
90
    public $questionFeedbackEnabled = false;
91
    public $questionTypeWithFeedback;
92
    public $showPreviousButton;
93
    public $notifications;
94
    public $export = false;
95
    public $autolaunch;
96
    public $quizCategoryId;
97
    public $pageResultConfiguration;
98
    public $hideQuestionNumber;
99
    public $preventBackwards;
100
    public $currentQuestion;
101
    public $hideComment;
102
    public $hideNoAnswer;
103
    public $hideExpectedAnswer;
104
    public $forceShowExpectedChoiceColumn;
105
    public $disableHideCorrectAnsweredQuestions;
106
107
    /**
108
     * @param int $courseId
109
     */
110
    public function __construct($courseId = 0)
111
    {
112
        $this->iId = 0;
113
        $this->id = 0;
114
        $this->exercise = '';
115
        $this->description = '';
116
        $this->sound = '';
117
        $this->type = ALL_ON_ONE_PAGE;
118
        $this->random = 0;
119
        $this->random_answers = 0;
120
        $this->active = 1;
121
        $this->questionList = [];
122
        $this->timeLimit = 0;
123
        $this->end_time = '';
124
        $this->start_time = '';
125
        $this->results_disabled = 1;
126
        $this->expired_time = 0;
127
        $this->propagate_neg = 0;
128
        $this->saveCorrectAnswers = 0;
129
        $this->review_answers = false;
130
        $this->randomByCat = 0;
131
        $this->text_when_finished = '';
132
        $this->text_when_finished_failure = '';
133
        $this->display_category_name = 0;
134
        $this->pass_percentage = 0;
135
        $this->modelType = 1;
136
        $this->questionSelectionType = EX_Q_SELECTION_ORDERED;
137
        $this->endButton = 0;
138
        $this->scoreTypeModel = 0;
139
        $this->globalCategoryId = null;
140
        $this->notifications = [];
141
        $this->quizCategoryId = 0;
142
        $this->pageResultConfiguration = null;
143
        $this->hideQuestionNumber = 0;
144
        $this->preventBackwards = 0;
145
        $this->hideComment = false;
146
        $this->hideNoAnswer = false;
147
        $this->hideExpectedAnswer = false;
148
        $this->disableHideCorrectAnsweredQuestions = false;
149
150
        if (empty($course_id)) {
151
            $course_id = api_get_course_int_id();
152
        }
153
154
        $this->course = api_get_course_info_by_id($course_id);
155
        $this->course_id = $course_id;
156
157
        $this->sessionId = api_get_session_id();
158
159
        // ALTER TABLE c_quiz_question ADD COLUMN feedback text;
160
        $this->questionFeedbackEnabled = ('true' === api_get_setting('exercise.allow_quiz_question_feedback'));
161
        $this->showPreviousButton = true;
162
    }
163
164
    /**
165
     * Reads exercise information from the data base.
166
     *
167
     * @param int  $id                - exercise Id
168
     * @param bool $parseQuestionList
169
     *
170
     * @return bool - true if exercise exists, otherwise false
171
     */
172
    public function read($id, $parseQuestionList = true)
173
    {
174
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
175
176
        $id = (int) $id;
177
        if (empty($this->course_id) || empty($id)) {
178
            return false;
179
        }
180
181
        $sql = "SELECT * FROM $table
182
                WHERE iid = $id";
183
        $result = Database::query($sql);
184
185
        // if the exercise has been found
186
        if ($object = Database::fetch_object($result)) {
187
            $this->id = $this->iId = (int) $object->iid;
188
            $this->exercise = $object->title;
189
            $this->name = $object->title;
190
            $this->title = $object->title;
191
            $this->description = $object->description;
192
            $this->sound = $object->sound;
193
            $this->type = $object->type;
194
            if (empty($this->type)) {
195
                $this->type = ONE_PER_PAGE;
196
            }
197
            $this->random = $object->random;
198
            $this->random_answers = $object->random_answers;
199
            $this->results_disabled = $object->results_disabled;
200
            $this->attempts = $object->max_attempt;
201
            $this->feedback_type = $object->feedback_type;
202
            //$this->sessionId = $object->session_id;
203
            $this->propagate_neg = $object->propagate_neg;
204
            $this->saveCorrectAnswers = $object->save_correct_answers;
205
            $this->randomByCat = $object->random_by_category;
206
            $this->text_when_finished = $object->text_when_finished;
207
            $this->text_when_finished_failure = $object->text_when_finished_failure;
208
            $this->display_category_name = $object->display_category_name;
209
            $this->pass_percentage = $object->pass_percentage;
210
            $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
211
            $this->review_answers = isset($object->review_answers) && 1 == $object->review_answers ? true : false;
212
            $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null;
213
            $this->questionSelectionType = isset($object->question_selection_type) ? (int) $object->question_selection_type : null;
214
            $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0;
215
            $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0;
216
            $this->quizCategoryId = isset($object->quiz_category_id) ? (int) $object->quiz_category_id : null;
217
            $this->preventBackwards = isset($object->prevent_backwards) ? (int) $object->prevent_backwards : 0;
218
            $this->exercise_was_added_in_lp = false;
219
            $this->lpList = [];
220
            $this->notifications = [];
221
            if (!empty($object->notifications)) {
222
                $this->notifications = explode(',', $object->notifications);
223
            }
224
225
            if (!empty($object->page_result_configuration)) {
226
                //$this->pageResultConfiguration = $object->page_result_configuration;
227
            }
228
229
            $this->hideQuestionNumber = 1 == $object->hide_question_number;
230
231
            if (isset($object->show_previous_button)) {
232
                $this->showPreviousButton = 1 == $object->show_previous_button ? true : false;
233
            }
234
235
            $list = self::getLpListFromExercise($id, $this->course_id);
236
            if (!empty($list)) {
237
                $this->exercise_was_added_in_lp = true;
238
                $this->lpList = $list;
239
            }
240
241
            $this->force_edit_exercise_in_lp = api_get_setting('lp.force_edit_exercise_in_lp');
242
            $this->edit_exercise_in_lp = true;
243
            if ($this->exercise_was_added_in_lp) {
244
                $this->edit_exercise_in_lp = true == $this->force_edit_exercise_in_lp;
245
            }
246
247
            if (!empty($object->end_time)) {
248
                $this->end_time = $object->end_time;
249
            }
250
            if (!empty($object->start_time)) {
251
                $this->start_time = $object->start_time;
252
            }
253
254
            // Control time
255
            $this->expired_time = $object->expired_time;
256
257
            // Checking if question_order is correctly set
258
            if ($parseQuestionList) {
259
                $this->setQuestionList(true);
260
            }
261
262
            //overload questions list with recorded questions list
263
            //load questions only for exercises of type 'one question per page'
264
            //this is needed only is there is no questions
265
266
            // @todo not sure were in the code this is used somebody mess with the exercise tool
267
            // @todo don't know who add that config and why $_configuration['live_exercise_tracking']
268
            /*global $_configuration, $questionList;
269
            if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST'
270
                && defined('QUESTION_LIST_ALREADY_LOGGED') &&
271
                isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking']
272
            ) {
273
                $this->questionList = $questionList;
274
            }*/
275
276
            return true;
277
        }
278
279
        return false;
280
    }
281
282
    public function getCutTitle(): string
283
    {
284
        $title = $this->getUnformattedTitle();
285
286
        return cut($title, EXERCISE_MAX_NAME_SIZE);
287
    }
288
289
    public function getId()
290
    {
291
        return (int) $this->iId;
292
    }
293
294
    /**
295
     * returns the exercise title.
296
     *
297
     * @param bool $unformattedText Optional. Get the title without HTML tags
298
     *
299
     * @return string - exercise title
300
     */
301
    public function selectTitle($unformattedText = false)
302
    {
303
        if ($unformattedText) {
304
            return $this->getUnformattedTitle();
305
        }
306
307
        return $this->exercise;
308
    }
309
310
    /**
311
     * returns the number of attempts setted.
312
     *
313
     * @return int - exercise attempts
314
     */
315
    public function selectAttempts()
316
    {
317
        return $this->attempts;
318
    }
319
320
    /**
321
     * Returns the number of FeedbackType
322
     *  0: Feedback , 1: DirectFeedback, 2: NoFeedback.
323
     *
324
     * @return int - exercise attempts
325
     */
326
    public function getFeedbackType()
327
    {
328
        return (int) $this->feedback_type;
329
    }
330
331
    /**
332
     * returns the time limit.
333
     *
334
     * @return int
335
     */
336
    public function selectTimeLimit()
337
    {
338
        return $this->timeLimit;
339
    }
340
341
    /**
342
     * returns the exercise description.
343
     *
344
     * @return string - exercise description
345
     */
346
    public function selectDescription()
347
    {
348
        return $this->description;
349
    }
350
351
    /**
352
     * returns the exercise sound file.
353
     */
354
    public function getSound()
355
    {
356
        return $this->sound;
357
    }
358
359
    /**
360
     * returns the exercise type.
361
     *
362
     * @return int - exercise type
363
     *
364
     * @author Olivier Brouckaert
365
     */
366
    public function selectType()
367
    {
368
        return $this->type;
369
    }
370
371
    /**
372
     * @return int
373
     */
374
    public function getModelType()
375
    {
376
        return $this->modelType;
377
    }
378
379
    /**
380
     * @return int
381
     */
382
    public function selectEndButton()
383
    {
384
        return $this->endButton;
385
    }
386
387
    /**
388
     * @return int : do we display the question category name for students
389
     *
390
     * @author hubert borderiou 30-11-11
391
     */
392
    public function selectDisplayCategoryName()
393
    {
394
        return $this->display_category_name;
395
    }
396
397
    /**
398
     * @return int
399
     */
400
    public function selectPassPercentage()
401
    {
402
        return $this->pass_percentage;
403
    }
404
405
    /**
406
     * Modify object to update the switch display_category_name.
407
     *
408
     * @param int $value is an integer 0 or 1
409
     *
410
     * @author hubert borderiou 30-11-11
411
     */
412
    public function updateDisplayCategoryName($value)
413
    {
414
        $this->display_category_name = $value;
415
    }
416
417
    /**
418
     * @return string html text : the text to display ay the end of the test
419
     *
420
     * @author hubert borderiou 28-11-11
421
     */
422
    public function getTextWhenFinished(): string
423
    {
424
        return $this->text_when_finished;
425
    }
426
427
    /**
428
     * @param string $text
429
     *
430
     * @author hubert borderiou 28-11-11
431
     */
432
    public function setTextWhenFinished(string $text): void
433
    {
434
        $this->text_when_finished = $text;
435
    }
436
437
    /**
438
     * Get the text to display when the user has failed the test
439
     * @return string html text : the text to display ay the end of the test
440
     */
441
    public function getTextWhenFinishedFailure(): string
442
    {
443
        if (empty($this->text_when_finished_failure)) {
444
            return '';
445
        }
446
447
        return $this->text_when_finished_failure;
448
    }
449
450
    /**
451
     * Set the text to display when the user has succeeded in the test
452
     * @param string $text
453
     */
454
    public function setTextWhenFinishedFailure(string $text): void
455
    {
456
        $this->text_when_finished_failure = $text;
457
    }
458
459
    /**
460
     * return 1 or 2 if randomByCat.
461
     *
462
     * @return int - quiz random by category
463
     *
464
     * @author hubert borderiou
465
     */
466
    public function getRandomByCategory()
467
    {
468
        return $this->randomByCat;
469
    }
470
471
    /**
472
     * return 0 if no random by cat
473
     * return 1 if random by cat, categories shuffled
474
     * return 2 if random by cat, categories sorted by alphabetic order.
475
     *
476
     * @return int - quiz random by category
477
     *
478
     * @author hubert borderiou
479
     */
480
    public function isRandomByCat()
481
    {
482
        $res = EXERCISE_CATEGORY_RANDOM_DISABLED;
483
        if (EXERCISE_CATEGORY_RANDOM_SHUFFLED == $this->randomByCat) {
484
            $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED;
485
        } elseif (EXERCISE_CATEGORY_RANDOM_ORDERED == $this->randomByCat) {
486
            $res = EXERCISE_CATEGORY_RANDOM_ORDERED;
487
        }
488
489
        return $res;
490
    }
491
492
    /**
493
     * return nothing
494
     * update randomByCat value for object.
495
     *
496
     * @param int $random
497
     *
498
     * @author hubert borderiou
499
     */
500
    public function updateRandomByCat($random)
501
    {
502
        $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED;
503
        if (in_array(
504
            $random,
505
            [
506
                EXERCISE_CATEGORY_RANDOM_SHUFFLED,
507
                EXERCISE_CATEGORY_RANDOM_ORDERED,
508
                EXERCISE_CATEGORY_RANDOM_DISABLED,
509
            ]
510
        )) {
511
            $this->randomByCat = $random;
512
        }
513
    }
514
515
    /**
516
     * Tells if questions are selected randomly, and if so returns the draws.
517
     *
518
     * @return int - results disabled exercise
519
     *
520
     * @author Carlos Vargas
521
     */
522
    public function selectResultsDisabled()
523
    {
524
        return $this->results_disabled;
525
    }
526
527
    /**
528
     * tells if questions are selected randomly, and if so returns the draws.
529
     *
530
     * @return bool
531
     *
532
     * @author Olivier Brouckaert
533
     */
534
    public function isRandom()
535
    {
536
        $isRandom = false;
537
        // "-1" means all questions will be random
538
        if ($this->random > 0 || -1 == $this->random) {
539
            $isRandom = true;
540
        }
541
542
        return $isRandom;
543
    }
544
545
    /**
546
     * returns random answers status.
547
     *
548
     * @author Juan Carlos Rana
549
     */
550
    public function getRandomAnswers()
551
    {
552
        return $this->random_answers;
553
    }
554
555
    /**
556
     * Same as isRandom() but has a name applied to values different than 0 or 1.
557
     *
558
     * @return int
559
     */
560
    public function getShuffle()
561
    {
562
        return $this->random;
563
    }
564
565
    /**
566
     * returns the exercise status (1 = enabled ; 0 = disabled).
567
     *
568
     * @return int - 1 if enabled, otherwise 0
569
     *
570
     * @author Olivier Brouckaert
571
     */
572
    public function selectStatus()
573
    {
574
        return $this->active;
575
    }
576
577
    /**
578
     * If false the question list will be managed as always if true
579
     * the question will be filtered
580
     * depending of the exercise settings (table c_quiz_rel_category).
581
     *
582
     * @param bool $status active or inactive grouping
583
     */
584
    public function setCategoriesGrouping($status)
585
    {
586
        $this->categories_grouping = (bool) $status;
587
    }
588
589
    /**
590
     * @return int
591
     */
592
    public function getHideQuestionTitle()
593
    {
594
        return $this->hideQuestionTitle;
595
    }
596
597
    /**
598
     * @param $value
599
     */
600
    public function setHideQuestionTitle($value)
601
    {
602
        $this->hideQuestionTitle = (int) $value;
603
    }
604
605
    /**
606
     * @return int
607
     */
608
    public function getScoreTypeModel()
609
    {
610
        return $this->scoreTypeModel;
611
    }
612
613
    /**
614
     * @param int $value
615
     */
616
    public function setScoreTypeModel($value)
617
    {
618
        $this->scoreTypeModel = (int) $value;
619
    }
620
621
    /**
622
     * @return int
623
     */
624
    public function getGlobalCategoryId()
625
    {
626
        return $this->globalCategoryId;
627
    }
628
629
    /**
630
     * @param int $value
631
     */
632
    public function setGlobalCategoryId($value)
633
    {
634
        if (is_array($value) && isset($value[0])) {
635
            $value = $value[0];
636
        }
637
        $this->globalCategoryId = (int) $value;
638
    }
639
640
    /**
641
     * @param int    $start
642
     * @param int    $limit
643
     * @param string $sidx
644
     * @param string $sord
645
     * @param array  $whereCondition
646
     * @param array  $extraFields
647
     *
648
     * @return array
649
     */
650
    public function getQuestionListPagination(
651
        $start,
652
        $limit,
653
        $sidx,
654
        $sord,
655
        $whereCondition = [],
656
        $extraFields = []
657
    ) {
658
        if (!empty($this->id)) {
659
            $category_list = TestCategory::getListOfCategoriesNameForTest(
660
                $this->id,
661
                false
662
            );
663
            $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
664
            $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
665
666
            $sql = "SELECT q.iid
667
                    FROM $TBL_EXERCICE_QUESTION e
668
                    INNER JOIN $TBL_QUESTIONS  q
669
                    ON (e.question_id = q.iid)
670
					WHERE e.quiz_id	= '".$this->id."' ";
671
672
            $orderCondition = ' ORDER BY question_order ';
673
674
            if (!empty($sidx) && !empty($sord)) {
675
                if ('question' === $sidx) {
676
                    if (in_array(strtolower($sord), ['desc', 'asc'])) {
677
                        $orderCondition = " ORDER BY `q.$sidx` $sord";
678
                    }
679
                }
680
            }
681
682
            $sql .= $orderCondition;
683
            $limitCondition = null;
684
            if (isset($start) && isset($limit)) {
685
                $start = (int) $start;
686
                $limit = (int) $limit;
687
                $limitCondition = " LIMIT $start, $limit";
688
            }
689
            $sql .= $limitCondition;
690
            $result = Database::query($sql);
691
            $questions = [];
692
            if (Database::num_rows($result)) {
693
                if (!empty($extraFields)) {
694
                    $extraFieldValue = new ExtraFieldValue('question');
695
                }
696
                while ($question = Database::fetch_assoc($result)) {
697
                    /** @var Question $objQuestionTmp */
698
                    $objQuestionTmp = Question::read($question['iid']);
699
                    $category_labels = '';
700
                    // @todo not implemented in 1.11.x
701
                    /*$category_labels = TestCategory::return_category_labels(
702
                        $objQuestionTmp->category_list,
703
                        $category_list
704
                    );*/
705
706
                    if (empty($category_labels)) {
707
                        $category_labels = '-';
708
                    }
709
710
                    // Question type
711
                    $typeImg = $objQuestionTmp->getTypePicture();
712
                    $typeExpl = $objQuestionTmp->getExplanation();
713
714
                    $question_media = null;
715
                    if (!empty($objQuestionTmp->parent_id)) {
716
                        // @todo not implemented in 1.11.x
717
                        //$objQuestionMedia = Question::read($objQuestionTmp->parent_id);
718
                        //$question_media = Question::getMediaLabel($objQuestionMedia->question);
719
                    }
720
721
                    $questionType = Display::tag(
722
                        'div',
723
                        Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media
724
                    );
725
726
                    $question = [
727
                        'id' => $question['iid'],
728
                        'question' => $objQuestionTmp->selectTitle(),
729
                        'type' => $questionType,
730
                        'category' => Display::tag(
731
                            'div',
732
                            '<a href="#" style="padding:0px; margin:0px;">'.$category_labels.'</a>'
733
                        ),
734
                        'score' => $objQuestionTmp->selectWeighting(),
735
                        'level' => $objQuestionTmp->level,
736
                    ];
737
738
                    if (!empty($extraFields)) {
739
                        foreach ($extraFields as $extraField) {
740
                            $value = $extraFieldValue->get_values_by_handler_and_field_id(
741
                                $question['id'],
742
                                $extraField['id']
743
                            );
744
                            $stringValue = null;
745
                            if ($value) {
746
                                $stringValue = $value['field_value'];
747
                            }
748
                            $question[$extraField['field_variable']] = $stringValue;
749
                        }
750
                    }
751
                    $questions[] = $question;
752
                }
753
            }
754
755
            return $questions;
756
        }
757
    }
758
759
    /**
760
     * Get question count per exercise from DB (any special treatment).
761
     *
762
     * @return int
763
     */
764
    public function getQuestionCount()
765
    {
766
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
767
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
768
        $sql = "SELECT count(q.iid) as count
769
                FROM $TBL_EXERCICE_QUESTION e
770
                INNER JOIN $TBL_QUESTIONS q
771
                ON (e.question_id = q.iid)
772
                WHERE
773
                    e.quiz_id = ".$this->getId();
774
        $result = Database::query($sql);
775
776
        $count = 0;
777
        if (Database::num_rows($result)) {
778
            $row = Database::fetch_array($result);
779
            $count = (int) $row['count'];
780
        }
781
782
        return $count;
783
    }
784
785
    /**
786
     * @return array
787
     */
788
    public function getQuestionOrderedListByName()
789
    {
790
        if (empty($this->course_id) || empty($this->getId())) {
791
            return [];
792
        }
793
794
        $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
795
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
796
797
        // Getting question list from the order (question list drag n drop interface ).
798
        $sql = "SELECT e.question_id
799
                FROM $exerciseQuestionTable e
800
                INNER JOIN $questionTable q
801
                ON (e.question_id= q.iid)
802
                WHERE
803
                    e.quiz_id = '".$this->getId()."'
804
                ORDER BY q.question";
805
        $result = Database::query($sql);
806
        $list = [];
807
        if (Database::num_rows($result)) {
808
            $list = Database::store_result($result, 'ASSOC');
809
        }
810
811
        return $list;
812
    }
813
814
    /**
815
     * Selecting question list depending in the exercise-category
816
     * relationship (category table in exercise settings).
817
     *
818
     * @param array $questionList
819
     * @param int   $questionSelectionType
820
     *
821
     * @return array
822
     */
823
    public function getQuestionListWithCategoryListFilteredByCategorySettings(
824
        $questionList,
825
        $questionSelectionType
826
    ) {
827
        $result = [
828
            'question_list' => [],
829
            'category_with_questions_list' => [],
830
        ];
831
832
        // Order/random categories
833
        $cat = new TestCategory();
834
835
        // Setting category order.
836
        switch ($questionSelectionType) {
837
            case EX_Q_SELECTION_ORDERED: // 1
838
            case EX_Q_SELECTION_RANDOM:  // 2
839
                // This options are not allowed here.
840
                break;
841
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3
842
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
843
                    $this,
844
                    'title ASC',
845
                    false,
846
                    true
847
                );
848
849
                $questionsByCategory = TestCategory::getQuestionsByCat(
850
                    $this->getId(),
851
                    $questionList,
852
                    $categoriesAddedInExercise
853
                );
854
855
                $questionList = $this->pickQuestionsPerCategory(
856
                    $categoriesAddedInExercise,
857
                    $questionList,
858
                    $questionsByCategory,
859
                    true,
860
                    false
861
                );
862
863
                break;
864
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4
865
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
866
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
867
                    $this,
868
                    null,
869
                    true,
870
                    true
871
                );
872
                $questionsByCategory = TestCategory::getQuestionsByCat(
873
                    $this->getId(),
874
                    $questionList,
875
                    $categoriesAddedInExercise
876
                );
877
                $questionList = $this->pickQuestionsPerCategory(
878
                    $categoriesAddedInExercise,
879
                    $questionList,
880
                    $questionsByCategory,
881
                    true,
882
                    false
883
                );
884
885
                break;
886
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5
887
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
888
                    $this,
889
                    'title ASC',
890
                    false,
891
                    true
892
                );
893
                $questionsByCategory = TestCategory::getQuestionsByCat(
894
                    $this->getId(),
895
                    $questionList,
896
                    $categoriesAddedInExercise
897
                );
898
                $questionsByCategoryMandatory = [];
899
                if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $this->getQuestionSelectionType() &&
900
                    ('true' === api_get_setting('exercise.allow_mandatory_question_in_category'))
901
                ) {
902
                    $questionsByCategoryMandatory = TestCategory::getQuestionsByCat(
903
                        $this->id,
904
                        $questionList,
905
                        $categoriesAddedInExercise,
906
                        true
907
                    );
908
                }
909
                $questionList = $this->pickQuestionsPerCategory(
910
                    $categoriesAddedInExercise,
911
                    $questionList,
912
                    $questionsByCategory,
913
                    true,
914
                    true,
915
                    $questionsByCategoryMandatory
916
                );
917
918
                break;
919
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6
920
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED:
921
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
922
                    $this,
923
                    null,
924
                    true,
925
                    true
926
                );
927
928
                $questionsByCategory = TestCategory::getQuestionsByCat(
929
                    $this->getId(),
930
                    $questionList,
931
                    $categoriesAddedInExercise
932
                );
933
934
                $questionList = $this->pickQuestionsPerCategory(
935
                    $categoriesAddedInExercise,
936
                    $questionList,
937
                    $questionsByCategory,
938
                    true,
939
                    true
940
                );
941
942
                break;
943
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9
944
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
945
                    $this,
946
                    'root ASC, lft ASC',
947
                    false,
948
                    true
949
                );
950
                $questionsByCategory = TestCategory::getQuestionsByCat(
951
                    $this->getId(),
952
                    $questionList,
953
                    $categoriesAddedInExercise
954
                );
955
                $questionList = $this->pickQuestionsPerCategory(
956
                    $categoriesAddedInExercise,
957
                    $questionList,
958
                    $questionsByCategory,
959
                    true,
960
                    false
961
                );
962
963
                break;
964
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10
965
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
966
                    $this,
967
                    'root, lft ASC',
968
                    false,
969
                    true
970
                );
971
                $questionsByCategory = TestCategory::getQuestionsByCat(
972
                    $this->getId(),
973
                    $questionList,
974
                    $categoriesAddedInExercise
975
                );
976
                $questionList = $this->pickQuestionsPerCategory(
977
                    $categoriesAddedInExercise,
978
                    $questionList,
979
                    $questionsByCategory,
980
                    true,
981
                    true
982
                );
983
984
                break;
985
        }
986
987
        $result['question_list'] = $questionList ?? [];
988
        $result['category_with_questions_list'] = $questionsByCategory ?? [];
989
        $parentsLoaded = [];
990
        // Adding category info in the category list with question list:
991
        if (!empty($questionsByCategory)) {
992
            $newCategoryList = [];
993
            $em = Database::getManager();
994
            $repo = $em->getRepository(CQuizRelQuestionCategory::class);
995
996
            foreach ($questionsByCategory as $categoryId => $questionList) {
997
                $category = new TestCategory();
998
                $cat = (array) $category->getCategory($categoryId);
999
                if ($cat) {
1000
                    $cat['iid'] = $cat['id'];
1001
                }
1002
1003
                $categoryParentInfo = null;
1004
                // Parent is not set no loop here
1005
                if (isset($cat['parent_id']) && !empty($cat['parent_id'])) {
1006
                    /** @var CQuizRelQuestionCategory $categoryEntity */
1007
                    if (!isset($parentsLoaded[$cat['parent_id']])) {
1008
                        $categoryEntity = $em->find(CQuizRelQuestionCategory::class, $cat['parent_id']);
1009
                        $parentsLoaded[$cat['parent_id']] = $categoryEntity;
1010
                    } else {
1011
                        $categoryEntity = $parentsLoaded[$cat['parent_id']];
1012
                    }
1013
                    $path = $repo->getPath($categoryEntity);
1014
1015
                    $index = 0;
1016
                    if ($this->categoryMinusOne) {
1017
                        //$index = 1;
1018
                    }
1019
1020
                    /** @var CQuizRelQuestionCategory $categoryParent */
1021
                    // @todo not implemented in 1.11.x
1022
                    /*foreach ($path as $categoryParent) {
1023
                        $visibility = $categoryParent->getVisibility();
1024
                        if (0 == $visibility) {
1025
                            $categoryParentId = $categoryId;
1026
                            $categoryTitle = $cat['title'];
1027
                            if (count($path) > 1) {
1028
                                continue;
1029
                            }
1030
                        } else {
1031
                            $categoryParentId = $categoryParent->getIid();
1032
                            $categoryTitle = $categoryParent->getTitle();
1033
                        }
1034
1035
                        $categoryParentInfo['id'] = $categoryParentId;
1036
                        $categoryParentInfo['iid'] = $categoryParentId;
1037
                        $categoryParentInfo['parent_path'] = null;
1038
                        $categoryParentInfo['title'] = $categoryTitle;
1039
                        $categoryParentInfo['name'] = $categoryTitle;
1040
                        $categoryParentInfo['parent_id'] = null;
1041
1042
                        break;
1043
                    }*/
1044
                }
1045
                $cat['parent_info'] = $categoryParentInfo;
1046
                $newCategoryList[$categoryId] = [
1047
                    'category' => $cat,
1048
                    'question_list' => $questionList,
1049
                ];
1050
            }
1051
1052
            $result['category_with_questions_list'] = $newCategoryList;
1053
        }
1054
1055
        return $result;
1056
    }
1057
1058
    /**
1059
     * returns the array with the question ID list.
1060
     *
1061
     * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory
1062
     * @param bool $adminView    Whether we should return all questions (admin view) or
1063
     *                           just a list limited by the max number of random questions
1064
     *
1065
     * @return array - question ID list
1066
     */
1067
    public function selectQuestionList($fromDatabase = false, $adminView = false)
1068
    {
1069
        //var_dump($this->getId());exit;
1070
        if ($fromDatabase && !empty($this->getId())) {
1071
            $nbQuestions = $this->getQuestionCount();
1072
1073
            $questionSelectionType = $this->getQuestionSelectionType();
1074
1075
            switch ($questionSelectionType) {
1076
                case EX_Q_SELECTION_ORDERED:
1077
                    $questionList = $this->getQuestionOrderedList($adminView);
1078
1079
                    break;
1080
                case EX_Q_SELECTION_RANDOM:
1081
                    // Not a random exercise, or if there are not at least 2 questions
1082
                    if (0 == $this->random || $nbQuestions < 2) {
1083
                        $questionList = $this->getQuestionOrderedList($adminView);
1084
                    } else {
1085
                        $questionList = $this->getRandomList($adminView);
1086
                    }
1087
1088
                    break;
1089
                default:
1090
                    $questionList = $this->getQuestionOrderedList($adminView);
1091
                    $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings(
1092
                        $questionList,
1093
                        $questionSelectionType
1094
                    );
1095
                    $this->categoryWithQuestionList = $result['category_with_questions_list'];
1096
                    $questionList = $result['question_list'];
1097
1098
                    break;
1099
            }
1100
1101
            return $questionList;
1102
        }
1103
1104
        return $this->questionList;
1105
    }
1106
1107
    /**
1108
     * returns the number of questions in this exercise.
1109
     *
1110
     * @return int - number of questions
1111
     */
1112
    public function selectNbrQuestions()
1113
    {
1114
        return count($this->questionList);
1115
    }
1116
1117
    /**
1118
     * @return int
1119
     */
1120
    public function selectPropagateNeg()
1121
    {
1122
        return $this->propagate_neg;
1123
    }
1124
1125
    /**
1126
     * @return int
1127
     */
1128
    public function getSaveCorrectAnswers()
1129
    {
1130
        return $this->saveCorrectAnswers;
1131
    }
1132
1133
    /**
1134
     * Selects questions randomly in the question list.
1135
     *
1136
     * @param bool $adminView Whether we should return all
1137
     *                        questions (admin view) or just a list limited by the max number of random questions
1138
     *
1139
     * @return array - if the exercise is not set to take questions randomly, returns the question list
1140
     *               without randomizing, otherwise, returns the list with questions selected randomly
1141
     *
1142
     * @author Olivier Brouckaert
1143
     * @author Hubert Borderiou 15 nov 2011
1144
     */
1145
    public function getRandomList($adminView = false)
1146
    {
1147
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1148
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1149
        $random = isset($this->random) && !empty($this->random) ? $this->random : 0;
1150
1151
        // Random with limit
1152
        $randomLimit = " ORDER BY RAND() LIMIT $random";
1153
1154
        // Random with no limit
1155
        if (-1 == $random) {
1156
            $randomLimit = ' ORDER BY RAND() ';
1157
        }
1158
1159
        // Admin see the list in default order
1160
        if (true === $adminView) {
1161
            // If viewing it as admin for edition, don't show it randomly, use title + id
1162
            $randomLimit = 'ORDER BY e.question_order';
1163
        }
1164
1165
        $sql = "SELECT e.question_id
1166
                FROM $quizRelQuestion e
1167
                INNER JOIN $question q
1168
                ON (e.question_id= q.iid)
1169
                WHERE
1170
                    e.quiz_id = '".$this->getId()."'
1171
                    $randomLimit ";
1172
        $result = Database::query($sql);
1173
        $questionList = [];
1174
        while ($row = Database::fetch_object($result)) {
1175
            $questionList[] = $row->question_id;
1176
        }
1177
1178
        return $questionList;
1179
    }
1180
1181
    /**
1182
     * returns 'true' if the question ID is in the question list.
1183
     *
1184
     * @param int $questionId - question ID
1185
     *
1186
     * @return bool - true if in the list, otherwise false
1187
     *
1188
     * @author Olivier Brouckaert
1189
     */
1190
    public function isInList($questionId)
1191
    {
1192
        $inList = false;
1193
        if (is_array($this->questionList)) {
1194
            $inList = in_array($questionId, $this->questionList);
1195
        }
1196
1197
        return $inList;
1198
    }
1199
1200
    /**
1201
     * If current exercise has a question.
1202
     *
1203
     * @param int $questionId
1204
     *
1205
     * @return int
1206
     */
1207
    public function hasQuestion($questionId)
1208
    {
1209
        $questionId = (int) $questionId;
1210
1211
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1212
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1213
        $sql = "SELECT q.iid
1214
                FROM $TBL_EXERCICE_QUESTION e
1215
                INNER JOIN $TBL_QUESTIONS q
1216
                ON (e.question_id = q.iid)
1217
                WHERE
1218
                    q.iid = $questionId AND
1219
                    e.quiz_id = ".$this->getId();
1220
1221
        $result = Database::query($sql);
1222
1223
        return Database::num_rows($result) > 0;
1224
    }
1225
1226
    public function hasQuestionWithType($type)
1227
    {
1228
        $type = (int) $type;
1229
1230
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1231
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1232
        $sql = "SELECT q.iid
1233
                FROM $table e
1234
                INNER JOIN $tableQuestion q
1235
                ON (e.question_id = q.iid)
1236
                WHERE
1237
                    q.type = $type AND
1238
                    e.quiz_id = ".$this->getId();
1239
1240
        $result = Database::query($sql);
1241
1242
        return Database::num_rows($result) > 0;
1243
    }
1244
1245
    public function hasQuestionWithTypeNotInList(array $questionTypeList)
1246
    {
1247
        if (empty($questionTypeList)) {
1248
            return false;
1249
        }
1250
1251
        $questionTypeToString = implode("','", array_map('intval', $questionTypeList));
1252
1253
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1254
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1255
        $sql = "SELECT q.iid
1256
                FROM $table e
1257
                INNER JOIN $tableQuestion q
1258
                ON (e.question_id = q.iid)
1259
                WHERE
1260
                    q.type NOT IN ('$questionTypeToString')  AND
1261
1262
                    e.quiz_id = ".$this->getId();
1263
1264
        $result = Database::query($sql);
1265
1266
        return Database::num_rows($result) > 0;
1267
    }
1268
1269
    /**
1270
     * changes the exercise title.
1271
     *
1272
     * @param string $title - exercise title
1273
     *
1274
     * @author Olivier Brouckaert
1275
     */
1276
    public function updateTitle($title)
1277
    {
1278
        $this->title = $this->exercise = $title;
1279
    }
1280
1281
    /**
1282
     * changes the exercise max attempts.
1283
     *
1284
     * @param int $attempts - exercise max attempts
1285
     */
1286
    public function updateAttempts($attempts)
1287
    {
1288
        $this->attempts = $attempts;
1289
    }
1290
1291
    /**
1292
     * changes the exercise feedback type.
1293
     *
1294
     * @param int $feedback_type
1295
     */
1296
    public function updateFeedbackType($feedback_type)
1297
    {
1298
        $this->feedback_type = $feedback_type;
1299
    }
1300
1301
    /**
1302
     * changes the exercise description.
1303
     *
1304
     * @param string $description - exercise description
1305
     *
1306
     * @author Olivier Brouckaert
1307
     */
1308
    public function updateDescription($description)
1309
    {
1310
        $this->description = $description;
1311
    }
1312
1313
    /**
1314
     * changes the exercise expired_time.
1315
     *
1316
     * @param int $expired_time The expired time of the quiz
1317
     *
1318
     * @author Isaac flores
1319
     */
1320
    public function updateExpiredTime($expired_time)
1321
    {
1322
        $this->expired_time = $expired_time;
1323
    }
1324
1325
    /**
1326
     * @param $value
1327
     */
1328
    public function updatePropagateNegative($value)
1329
    {
1330
        $this->propagate_neg = $value;
1331
    }
1332
1333
    /**
1334
     * @param int $value
1335
     */
1336
    public function updateSaveCorrectAnswers($value)
1337
    {
1338
        $this->saveCorrectAnswers = (int) $value;
1339
    }
1340
1341
    /**
1342
     * @param $value
1343
     */
1344
    public function updateReviewAnswers($value)
1345
    {
1346
        $this->review_answers = isset($value) && $value ? true : false;
1347
    }
1348
1349
    /**
1350
     * @param $value
1351
     */
1352
    public function updatePassPercentage($value)
1353
    {
1354
        $this->pass_percentage = $value;
1355
    }
1356
1357
    /**
1358
     * @param string $text
1359
     */
1360
    public function updateEmailNotificationTemplate($text)
1361
    {
1362
        $this->emailNotificationTemplate = $text;
1363
    }
1364
1365
    /**
1366
     * @param string $text
1367
     */
1368
    public function setEmailNotificationTemplateToUser($text)
1369
    {
1370
        $this->emailNotificationTemplateToUser = $text;
1371
    }
1372
1373
    /**
1374
     * @param string $value
1375
     */
1376
    public function setNotifyUserByEmail($value)
1377
    {
1378
        $this->notifyUserByEmail = $value;
1379
    }
1380
1381
    /**
1382
     * @param int $value
1383
     */
1384
    public function updateEndButton($value)
1385
    {
1386
        $this->endButton = (int) $value;
1387
    }
1388
1389
    /**
1390
     * @param string $value
1391
     */
1392
    public function setOnSuccessMessage($value)
1393
    {
1394
        $this->onSuccessMessage = $value;
1395
    }
1396
1397
    /**
1398
     * @param string $value
1399
     */
1400
    public function setOnFailedMessage($value)
1401
    {
1402
        $this->onFailedMessage = $value;
1403
    }
1404
1405
    /**
1406
     * @param $value
1407
     */
1408
    public function setModelType($value)
1409
    {
1410
        $this->modelType = (int) $value;
1411
    }
1412
1413
    /**
1414
     * @param int $value
1415
     */
1416
    public function setQuestionSelectionType($value)
1417
    {
1418
        $this->questionSelectionType = (int) $value;
1419
    }
1420
1421
    /**
1422
     * @return int
1423
     */
1424
    public function getQuestionSelectionType()
1425
    {
1426
        return (int) $this->questionSelectionType;
1427
    }
1428
1429
    /**
1430
     * @param array $categories
1431
     */
1432
    public function updateCategories($categories)
1433
    {
1434
        if (!empty($categories)) {
1435
            $categories = array_map('intval', $categories);
1436
            $this->categories = $categories;
1437
        }
1438
    }
1439
1440
    /**
1441
     * changes the exercise sound file.
1442
     *
1443
     * @param string $sound  - exercise sound file
1444
     * @param string $delete - ask to delete the file
1445
     *
1446
     * @author Olivier Brouckaert
1447
     */
1448
    public function updateSound($sound, $delete)
1449
    {
1450
        global $audioPath, $documentPath;
1451
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
1452
1453
        if ($sound['size'] &&
1454
            (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))
1455
        ) {
1456
            $this->sound = $sound['name'];
1457
1458
            if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
1459
                $sql = "SELECT 1 FROM $TBL_DOCUMENT
1460
                        WHERE
1461
                            c_id = ".$this->course_id." AND
1462
                            path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
1463
                $result = Database::query($sql);
1464
1465
                if (!Database::num_rows($result)) {
1466
                    DocumentManager::addDocument(
1467
                        $this->course,
1468
                        str_replace($documentPath, '', $audioPath).'/'.$this->sound,
1469
                        'file',
1470
                        $sound['size'],
1471
                        $sound['name']
1472
                    );
1473
                }
1474
            }
1475
        } elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
1476
            $this->sound = '';
1477
        }
1478
    }
1479
1480
    /**
1481
     * changes the exercise type.
1482
     *
1483
     * @param int $type - exercise type
1484
     *
1485
     * @author Olivier Brouckaert
1486
     */
1487
    public function updateType($type)
1488
    {
1489
        $this->type = $type;
1490
    }
1491
1492
    /**
1493
     * sets to 0 if questions are not selected randomly
1494
     * if questions are selected randomly, sets the draws.
1495
     *
1496
     * @param int $random - 0 if not random, otherwise the draws
1497
     *
1498
     * @author Olivier Brouckaert
1499
     */
1500
    public function setRandom($random)
1501
    {
1502
        $this->random = $random;
1503
    }
1504
1505
    /**
1506
     * sets to 0 if answers are not selected randomly
1507
     * if answers are selected randomly.
1508
     *
1509
     * @param int $random_answers - random answers
1510
     *
1511
     * @author Juan Carlos Rana
1512
     */
1513
    public function updateRandomAnswers($random_answers)
1514
    {
1515
        $this->random_answers = $random_answers;
1516
    }
1517
1518
    /**
1519
     * enables the exercise.
1520
     *
1521
     * @author Olivier Brouckaert
1522
     */
1523
    public function enable()
1524
    {
1525
        $this->active = 1;
1526
    }
1527
1528
    /**
1529
     * disables the exercise.
1530
     *
1531
     * @author Olivier Brouckaert
1532
     */
1533
    public function disable()
1534
    {
1535
        $this->active = 0;
1536
    }
1537
1538
    /**
1539
     * Set disable results.
1540
     */
1541
    public function disable_results()
1542
    {
1543
        $this->results_disabled = true;
1544
    }
1545
1546
    /**
1547
     * Enable results.
1548
     */
1549
    public function enable_results()
1550
    {
1551
        $this->results_disabled = false;
1552
    }
1553
1554
    /**
1555
     * @param int $results_disabled
1556
     */
1557
    public function updateResultsDisabled($results_disabled)
1558
    {
1559
        $this->results_disabled = (int) $results_disabled;
1560
    }
1561
1562
    /**
1563
     * updates the exercise in the data base.
1564
     *
1565
     * @author Olivier Brouckaert
1566
     */
1567
    public function save()
1568
    {
1569
        $id = $this->getId();
1570
        $title = $this->exercise;
1571
        $description = $this->description;
1572
        $sound = $this->sound;
1573
        $type = $this->type;
1574
        $attempts = isset($this->attempts) ? (int) $this->attempts : 0;
1575
        $feedback_type = isset($this->feedback_type) ? (int) $this->feedback_type : 0;
1576
        $random = $this->random;
1577
        $random_answers = $this->random_answers;
1578
        $propagate_neg = (int) $this->propagate_neg;
1579
        $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
1580
        $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
1581
        $randomByCat = (int) $this->randomByCat;
1582
        $text_when_finished = $this->text_when_finished;
1583
        $text_when_finished_failure = $this->text_when_finished_failure;
1584
        $display_category_name = (int) $this->display_category_name;
1585
        $pass_percentage = (int) $this->pass_percentage;
1586
1587
        // If direct we do not show results
1588
        $results_disabled = (int) $this->results_disabled;
1589
        if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1590
            $results_disabled = 0;
1591
        }
1592
        $expired_time = (int) $this->expired_time;
1593
1594
        $repo = Container::getQuizRepository();
1595
        $repoCategory = Container::getQuizCategoryRepository();
1596
1597
        // we prepare date in the database using the api_get_utc_datetime() function
1598
        $start_time = null;
1599
        if (!empty($this->start_time)) {
1600
            $start_time = $this->start_time;
1601
        }
1602
1603
        $end_time = null;
1604
        if (!empty($this->end_time)) {
1605
            $end_time = $this->end_time;
1606
        }
1607
1608
        // Exercise already exists
1609
        if ($id) {
1610
            /** @var CQuiz $exercise */
1611
            $exercise = $repo->find($id);
1612
        } else {
1613
            $exercise = new CQuiz();
1614
        }
1615
1616
        $exercise
1617
            ->setStartTime($start_time)
1618
            ->setEndTime($end_time)
1619
            ->setTitle($title)
1620
            ->setDescription($description)
1621
            ->setSound($sound)
1622
            ->setType($type)
1623
            ->setRandom((int) $random)
1624
            ->setRandomAnswers((bool) $random_answers)
1625
            ->setResultsDisabled($results_disabled)
1626
            ->setMaxAttempt($attempts)
1627
            ->setFeedbackType($feedback_type)
1628
            ->setExpiredTime($expired_time)
1629
            ->setReviewAnswers($review_answers)
1630
            ->setRandomByCategory($randomByCat)
1631
            ->setTextWhenFinished($text_when_finished)
1632
            ->setTextWhenFinishedFailure($text_when_finished_failure)
1633
            ->setDisplayCategoryName($display_category_name)
1634
            ->setPassPercentage($pass_percentage)
1635
            ->setSaveCorrectAnswers($saveCorrectAnswers)
1636
            ->setPropagateNeg($propagate_neg)
1637
            ->setHideQuestionTitle(1 === (int) $this->getHideQuestionTitle())
1638
            ->setQuestionSelectionType($this->getQuestionSelectionType())
1639
            ->setHideQuestionNumber((int) $this->hideQuestionNumber)
1640
        ;
1641
1642
        $allow = ('true' === api_get_setting('exercise.allow_exercise_categories'));
1643
        if (true === $allow && !empty($this->getQuizCategoryId())) {
1644
            $exercise->setQuizCategory($repoCategory->find($this->getQuizCategoryId()));
1645
        }
1646
1647
        $exercise->setPreventBackwards($this->getPreventBackwards());
1648
1649
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
1650
        if (true === $allow) {
1651
            $exercise->setShowPreviousButton($this->showPreviousButton());
1652
        }
1653
1654
        $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
1655
        if (true === $allow) {
1656
            $notifications = $this->getNotifications();
1657
            if (!empty($notifications)) {
1658
                $notifications = implode(',', $notifications);
1659
                $exercise->setNotifications($notifications);
1660
            }
1661
        }
1662
1663
        if (!empty($this->pageResultConfiguration)) {
1664
            $exercise->setPageResultConfiguration($this->pageResultConfiguration);
1665
        }
1666
1667
        $em = Database::getManager();
1668
1669
        if ($id) {
1670
            $repo->updateNodeForResource($exercise);
1671
1672
            if ('true' === api_get_setting('search_enabled')) {
1673
                $this->search_engine_edit();
1674
            }
1675
            $em->persist($exercise);
1676
            $em->flush();
1677
        } else {
1678
            // Creates a new exercise
1679
            $courseEntity = api_get_course_entity($this->course_id);
1680
            $exercise
1681
                ->setParent($courseEntity)
1682
                ->addCourseLink($courseEntity, api_get_session_entity());
1683
            $em->persist($exercise);
1684
            $em->flush();
1685
            $id = $exercise->getIid();
1686
            $this->iId = $this->id = $id;
1687
            if ($id) {
1688
                if ('true' === api_get_setting('search_enabled') && extension_loaded('xapian')) {
1689
                    $this->search_engine_save();
1690
                }
1691
            }
1692
        }
1693
1694
        $this->saveCategoriesInExercise($this->categories);
1695
1696
        return $id;
1697
    }
1698
1699
    /**
1700
     * Updates question position.
1701
     *
1702
     * @return bool
1703
     */
1704
    public function update_question_positions()
1705
    {
1706
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1707
        // Fixes #3483 when updating order
1708
        $questionList = $this->selectQuestionList(true);
1709
1710
        if (empty($this->getId())) {
1711
            return false;
1712
        }
1713
1714
        if (!empty($questionList)) {
1715
            foreach ($questionList as $position => $questionId) {
1716
                $position = (int) $position;
1717
                $questionId = (int) $questionId;
1718
                $sql = "UPDATE $table SET
1719
                            question_order = $position
1720
                        WHERE
1721
                            question_id = $questionId AND
1722
                            quiz_id= ".$this->getId();
1723
                Database::query($sql);
1724
            }
1725
        }
1726
1727
        return true;
1728
    }
1729
1730
    /**
1731
     * Adds a question into the question list.
1732
     *
1733
     * @param int $questionId - question ID
1734
     *
1735
     * @return bool - true if the question has been added, otherwise false
1736
     *
1737
     * @author Olivier Brouckaert
1738
     */
1739
    public function addToList($questionId)
1740
    {
1741
        // checks if the question ID is not in the list
1742
        if (!$this->isInList($questionId)) {
1743
            // selects the max position
1744
            if (!$this->selectNbrQuestions()) {
1745
                $pos = 1;
1746
            } else {
1747
                if (is_array($this->questionList)) {
1748
                    $pos = max(array_keys($this->questionList)) + 1;
1749
                }
1750
            }
1751
            $this->questionList[$pos] = $questionId;
1752
1753
            return true;
1754
        }
1755
1756
        return false;
1757
    }
1758
1759
    /**
1760
     * removes a question from the question list.
1761
     *
1762
     * @param int $questionId - question ID
1763
     *
1764
     * @return bool - true if the question has been removed, otherwise false
1765
     *
1766
     * @author Olivier Brouckaert
1767
     */
1768
    public function removeFromList($questionId)
1769
    {
1770
        // searches the position of the question ID in the list
1771
        $pos = array_search($questionId, $this->questionList);
1772
        // question not found
1773
        if (false === $pos) {
1774
            return false;
1775
        } else {
1776
            // dont reduce the number of random question if we use random by category option, or if
1777
            // random all questions
1778
            if ($this->isRandom() && 0 == $this->isRandomByCat()) {
1779
                if (count($this->questionList) >= $this->random && $this->random > 0) {
1780
                    $this->random--;
1781
                    $this->save();
1782
                }
1783
            }
1784
            // deletes the position from the array containing the wanted question ID
1785
            unset($this->questionList[$pos]);
1786
1787
            return true;
1788
        }
1789
    }
1790
1791
    /**
1792
     * deletes the exercise from the database
1793
     * Notice : leaves the question in the data base.
1794
     *
1795
     * @author Olivier Brouckaert
1796
     */
1797
    public function delete()
1798
    {
1799
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
1800
1801
        if ($limitTeacherAccess && !api_is_platform_admin()) {
1802
            return false;
1803
        }
1804
1805
        $exerciseId = $this->iId;
1806
1807
        $repo = Container::getQuizRepository();
1808
        /** @var CQuiz $exercise */
1809
        $exercise = $repo->find($exerciseId);
1810
        /** @var ResourceLinkRepository $linksRepo */
1811
        $linksRepo = Container::$container->get(ResourceLinkRepository::class);
1812
1813
        if (null === $exercise) {
1814
            return false;
1815
        }
1816
1817
        $locked = api_resource_is_locked_by_gradebook(
1818
            $exerciseId,
1819
            LINK_EXERCISE
1820
        );
1821
1822
        if ($locked) {
1823
            return false;
1824
        }
1825
1826
        $course = api_get_course_entity();
1827
        $session = api_get_session_entity();
1828
1829
        $linksRepo->removeByResourceInContext($exercise, $course, $session);
1830
1831
        SkillModel::deleteSkillsFromItem($exerciseId, ITEM_TYPE_EXERCISE);
1832
1833
        if ('true' === api_get_setting('search_enabled') &&
1834
            extension_loaded('xapian')
1835
        ) {
1836
            $this->search_engine_delete();
1837
        }
1838
1839
        $linkInfo = GradebookUtils::isResourceInCourseGradebook(
1840
            $this->course_id,
1841
            LINK_EXERCISE,
1842
            $exerciseId,
1843
            $this->sessionId
1844
        );
1845
        if (!empty($linkInfo)) {
1846
            GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
1847
        }
1848
1849
        // Register resource deletion manually because this is a soft delete (active = -1)
1850
        // and Doctrine does not trigger postRemove in this case.
1851
        /* @var TrackEDefaultRepository $trackRepo */
1852
        $trackRepo = Container::$container->get(TrackEDefaultRepository::class);
1853
        $resourceNode = $exercise->getResourceNode();
1854
        if ($resourceNode) {
1855
            $trackRepo->registerResourceEvent(
1856
                $resourceNode,
1857
                'deletion',
1858
                api_get_user_id(),
1859
                api_get_course_int_id(),
1860
                api_get_session_id()
1861
            );
1862
        }
1863
1864
        return true;
1865
    }
1866
1867
    /**
1868
     * Creates the form to create / edit an exercise.
1869
     *
1870
     * @param FormValidator $form
1871
     * @param string|array        $type
1872
     */
1873
    public function createForm($form, $type = 'full')
1874
    {
1875
        if (empty($type)) {
1876
            $type = 'full';
1877
        }
1878
1879
        // Form title
1880
        $form_title = get_lang('Create a new test');
1881
        if (!empty($_GET['id'])) {
1882
            $form_title = get_lang('Edit test name and settings');
1883
        }
1884
1885
        $form->addHeader($form_title);
1886
1887
        // Title.
1888
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
1889
            $form->addHtmlEditor(
1890
                'exerciseTitle',
1891
                get_lang('Test name'),
1892
                false,
1893
                false,
1894
                ['ToolbarSet' => 'TitleAsHtml']
1895
            );
1896
        } else {
1897
            $form->addElement(
1898
                'text',
1899
                'exerciseTitle',
1900
                get_lang('Test name'),
1901
                ['id' => 'exercise_title']
1902
            );
1903
        }
1904
1905
        $form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings'));
1906
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1907
1908
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
1909
            $categoryManager = new ExerciseCategoryManager();
1910
            $categories = $categoryManager->getCategories(api_get_course_int_id());
1911
            $options = [];
1912
            if (!empty($categories)) {
1913
                /** @var CQuizCategory $category */
1914
                foreach ($categories as $category) {
1915
                    $options[$category->getId()] = $category->getTitle();
1916
                }
1917
            }
1918
1919
            $form->addSelect(
1920
                'quiz_category_id',
1921
                get_lang('Category'),
1922
                $options,
1923
                ['placeholder' => get_lang('Please select an option')]
1924
            );
1925
        }
1926
1927
        $editor_config = [
1928
            'ToolbarSet' => 'TestQuestionDescription',
1929
            'Width' => '100%',
1930
            'Height' => '150',
1931
        ];
1932
1933
        if (is_array($type)) {
1934
            $editor_config = array_merge($editor_config, $type);
1935
        }
1936
1937
        $form->addHtmlEditor(
1938
            'exerciseDescription',
1939
            get_lang('Give a context to the test'),
1940
            false,
1941
            false,
1942
            $editor_config
1943
        );
1944
1945
        $skillList = [];
1946
        if ('full' === $type) {
1947
            // Can't modify a DirectFeedback question.
1948
            if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1949
                $this->setResultFeedbackGroup($form);
1950
1951
                // Type of results display on the final page
1952
                $this->setResultDisabledGroup($form);
1953
1954
                // Type of questions disposition on page
1955
                $radios = [];
1956
                $radios[] = $form->createElement(
1957
                    'radio',
1958
                    'exerciseType',
1959
                    null,
1960
                    get_lang('All questions on one page'),
1961
                    '1',
1962
                    [
1963
                        'onclick' => 'check_per_page_all()',
1964
                        'id' => 'option_page_all',
1965
                    ]
1966
                );
1967
                $radios[] = $form->createElement(
1968
                    'radio',
1969
                    'exerciseType',
1970
                    null,
1971
                    get_lang('One question by page'),
1972
                    '2',
1973
                    [
1974
                        'onclick' => 'check_per_page_one()',
1975
                        'id' => 'option_page_one',
1976
                    ]
1977
                );
1978
1979
                $form->addGroup($radios, null, get_lang('Questions per page'));
1980
            } else {
1981
                // if is Direct feedback but has not questions we can allow to modify the question type
1982
                if (empty($this->iId) || 0 === $this->getQuestionCount()) {
1983
                    $this->setResultFeedbackGroup($form);
1984
                    $this->setResultDisabledGroup($form);
1985
1986
                    // Type of questions disposition on page
1987
                    $radios = [];
1988
                    $radios[] = $form->createElement(
1989
                        'radio',
1990
                        'exerciseType',
1991
                        null,
1992
                        get_lang('All questions on one page'),
1993
                        '1'
1994
                    );
1995
                    $radios[] = $form->createElement(
1996
                        'radio',
1997
                        'exerciseType',
1998
                        null,
1999
                        get_lang('One question by page'),
2000
                        '2'
2001
                    );
2002
                    $form->addGroup($radios, null, get_lang('Sequential'));
2003
                } else {
2004
                    $this->setResultFeedbackGroup($form, true);
2005
                    $group = $this->setResultDisabledGroup($form);
2006
                    $group->freeze();
2007
2008
                    // we force the options to the DirectFeedback exercisetype
2009
                    //$form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
2010
                    //$form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
2011
2012
                    // Type of questions disposition on page
2013
                    $radios[] = $form->createElement(
2014
                        'radio',
2015
                        'exerciseType',
2016
                        null,
2017
                        get_lang('All questions on one page'),
2018
                        '1',
2019
                        [
2020
                            'onclick' => 'check_per_page_all()',
2021
                            'id' => 'option_page_all',
2022
                        ]
2023
                    );
2024
                    $radios[] = $form->createElement(
2025
                        'radio',
2026
                        'exerciseType',
2027
                        null,
2028
                        get_lang('One question by page'),
2029
                        '2',
2030
                        [
2031
                            'onclick' => 'check_per_page_one()',
2032
                            'id' => 'option_page_one',
2033
                        ]
2034
                    );
2035
2036
                    $type_group = $form->addGroup($radios, null, get_lang('Questions per page'));
2037
                    $type_group->freeze();
2038
                }
2039
            }
2040
2041
            $option = [
2042
                EX_Q_SELECTION_ORDERED => get_lang('Ordered by user'),
2043
                //  Defined by user
2044
                EX_Q_SELECTION_RANDOM => get_lang('Random'),
2045
                // 1-10, All
2046
                'per_categories' => '--------'.get_lang('Using categories').'----------',
2047
                // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
2048
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang(
2049
                    'Ordered categories alphabetically with questions ordered'
2050
                ),
2051
                // A 123 B 456 C 78 (0, 1, all)
2052
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang(
2053
                    'Random categories with questions ordered'
2054
                ),
2055
                // C 78 B 456 A 123
2056
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang(
2057
                    'Ordered categories alphabetically with random questions'
2058
                ),
2059
                // A 321 B 654 C 87
2060
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang(
2061
                    'Random categories with random questions'
2062
                ),
2063
                // C 87 B 654 A 321
2064
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('Random categories with questions ordered (questions not grouped)'),
2065
                /*    B 456 C 78 A 123
2066
                        456 78 123
2067
                        123 456 78
2068
                */
2069
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('Random categories with random questions (questions not grouped)'),
2070
                /*
2071
                    A 123 B 456 C 78
2072
                    B 456 C 78 A 123
2073
                    B 654 C 87 A 321
2074
                    654 87 321
2075
                    165 842 73
2076
                */
2077
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('Ordered categories by parent with questions ordered'),
2078
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('Ordered categories by parent with random questions'),
2079
            ];
2080
2081
            $form->addSelect(
2082
                'question_selection_type',
2083
                [get_lang('Question selection type')],
2084
                $option,
2085
                [
2086
                    'id' => 'questionSelection',
2087
                    'onchange' => 'checkQuestionSelection()',
2088
                ]
2089
            );
2090
2091
            $group = [
2092
                $form->createElement(
2093
                    'checkbox',
2094
                    'hide_expected_answer',
2095
                    null,
2096
                    get_lang('Hide expected answers column')
2097
                ),
2098
                $form->createElement(
2099
                    'checkbox',
2100
                    'hide_total_score',
2101
                    null,
2102
                    get_lang('Hide total score')
2103
                ),
2104
                $form->createElement(
2105
                    'checkbox',
2106
                    'hide_question_score',
2107
                    null,
2108
                    get_lang('Hide question score')
2109
                ),
2110
                $form->createElement(
2111
                    'checkbox',
2112
                    'hide_category_table',
2113
                    null,
2114
                    get_lang('Hide category table')
2115
                ),
2116
                $form->createElement(
2117
                    'checkbox',
2118
                    'hide_correct_answered_questions',
2119
                    null,
2120
                    get_lang('Hide correct answered questions')
2121
                ),
2122
            ];
2123
            $form->addGroup($group, null, get_lang('Results page configuration'));
2124
2125
            $group = [
2126
                $form->createElement('radio', 'hide_question_number', null, get_lang('Yes'), '1'),
2127
                $form->createElement('radio', 'hide_question_number', null, get_lang('No'), '0'),
2128
            ];
2129
            $form->addGroup($group, null, get_lang('Hide question numbering'));
2130
2131
            $displayMatrix = 'none';
2132
            $displayRandom = 'none';
2133
            $selectionType = $this->getQuestionSelectionType();
2134
            switch ($selectionType) {
2135
                case EX_Q_SELECTION_RANDOM:
2136
                    $displayRandom = 'block';
2137
2138
                    break;
2139
                case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
2140
                    $displayMatrix = 'block';
2141
2142
                    break;
2143
            }
2144
2145
            $form->addHtml('<div id="hidden_random" style="display:'.$displayRandom.'">');
2146
            // Number of random question.
2147
            $max = $this->getId() > 0 ? $this->getQuestionCount() : 10;
2148
            $option = range(0, $max);
2149
            $option[0] = get_lang('No');
2150
            $option[-1] = get_lang('All');
2151
            $form->addSelect(
2152
                'randomQuestions',
2153
                [
2154
                    get_lang('Random questions'),
2155
                    get_lang("To randomize all questions choose 10. To disable randomization, choose \"Do not randomize\"."),
2156
                ],
2157
                $option,
2158
                ['id' => 'randomQuestions']
2159
            );
2160
            $form->addHtml('</div>');
2161
            $form->addHtml('<div id="hidden_matrix" style="display:'.$displayMatrix.'">');
2162
2163
            // Category selection.
2164
            $cat = new TestCategory();
2165
            $cat_form = $cat->returnCategoryForm($this);
2166
            if (empty($cat_form)) {
2167
                $cat_form = '<span class="label label-warning">'.get_lang('No categories defined').'</span>';
2168
            }
2169
            $form->addElement('label', null, $cat_form);
2170
            $form->addHtml('</div>');
2171
2172
            // Random answers.
2173
            $radios_random_answers = [
2174
                $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
2175
                $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
2176
            ];
2177
            $form->addGroup($radios_random_answers, null, get_lang('Shuffle answers'));
2178
2179
            // Category name.
2180
            $radio_display_cat_name = [
2181
                $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
2182
                $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
2183
            ];
2184
            $form->addGroup($radio_display_cat_name, null, get_lang('Display questions category'));
2185
2186
            // Hide question title.
2187
            $group = [
2188
                $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
2189
                $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
2190
            ];
2191
            $form->addGroup($group, null, get_lang('Hide question title'));
2192
2193
            $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
2194
2195
            if (true === $allow) {
2196
                // Hide question title.
2197
                $group = [
2198
                    $form->createElement(
2199
                        'radio',
2200
                        'show_previous_button',
2201
                        null,
2202
                        get_lang('Yes'),
2203
                        '1'
2204
                    ),
2205
                    $form->createElement(
2206
                        'radio',
2207
                        'show_previous_button',
2208
                        null,
2209
                        get_lang('No'),
2210
                        '0'
2211
                    ),
2212
                ];
2213
                $form->addGroup($group, null, get_lang('Show previous button'));
2214
            }
2215
2216
            $form->addElement(
2217
                'number',
2218
                'exerciseAttempts',
2219
                get_lang('Max number of attempts'),
2220
                null,
2221
                ['id' => 'exerciseAttempts']
2222
            );
2223
2224
            // Exercise time limit
2225
            $form->addElement(
2226
                'checkbox',
2227
                'activate_start_date_check',
2228
                null,
2229
                get_lang('Enable start time'),
2230
                ['onclick' => 'activate_start_date()']
2231
            );
2232
2233
            if (!empty($this->start_time)) {
2234
                $form->addElement('html', '<div id="start_date_div" style="display:block;">');
2235
            } else {
2236
                $form->addElement('html', '<div id="start_date_div" style="display:none;">');
2237
            }
2238
2239
            $form->addElement('date_time_picker', 'start_time');
2240
            $form->addElement('html', '</div>');
2241
            $form->addElement(
2242
                'checkbox',
2243
                'activate_end_date_check',
2244
                null,
2245
                get_lang('Enable end time'),
2246
                ['onclick' => 'activate_end_date()']
2247
            );
2248
2249
            if (!empty($this->end_time)) {
2250
                $form->addHtml('<div id="end_date_div" style="display:block;">');
2251
            } else {
2252
                $form->addHtml('<div id="end_date_div" style="display:none;">');
2253
            }
2254
2255
            $form->addElement('date_time_picker', 'end_time');
2256
            $form->addElement('html', '</div>');
2257
2258
            $display = 'block';
2259
            $form->addElement(
2260
                'checkbox',
2261
                'propagate_neg',
2262
                null,
2263
                get_lang('Propagate negative results between questions')
2264
            );
2265
2266
            $options = [
2267
                '' => get_lang('Please select an option'),
2268
                1 => get_lang('Save the correct answer for the next attempt'),
2269
                2 => get_lang('Pre-fill with answers from previous attempt'),
2270
            ];
2271
            $form->addSelect(
2272
                'save_correct_answers',
2273
                get_lang('Save answers'),
2274
                $options
2275
            );
2276
2277
            $form->addElement('html', '<div class="clear">&nbsp;</div>');
2278
            $form->addCheckBox('review_answers', null, get_lang('Review my answers'));
2279
            $form->addElement('html', '<div id="divtimecontrol"  style="display:'.$display.';">');
2280
2281
            // Timer control
2282
            $form->addElement(
2283
                'checkbox',
2284
                'enabletimercontrol',
2285
                null,
2286
                get_lang('Enable time control'),
2287
                [
2288
                    'onclick' => 'option_time_expired()',
2289
                    'id' => 'enabletimercontrol',
2290
                    'onload' => 'check_load_time()',
2291
                ]
2292
            );
2293
2294
            $expired_date = (int) $this->selectExpiredTime();
2295
2296
            if (('0' != $expired_date)) {
2297
                $form->addElement('html', '<div id="timercontrol" style="display:block;">');
2298
            } else {
2299
                $form->addElement('html', '<div id="timercontrol" style="display:none;">');
2300
            }
2301
            $form->addText(
2302
                'enabletimercontroltotalminutes',
2303
                get_lang('Total duration in minutes of the test'),
2304
                false,
2305
                [
2306
                    'id' => 'enabletimercontroltotalminutes',
2307
                    'cols-size' => [2, 2, 8],
2308
                ]
2309
            );
2310
            $form->addElement('html', '</div>');
2311
            $form->addCheckBox('prevent_backwards', null, get_lang('Prevent moving backwards between questions'));
2312
            $form->addElement(
2313
                'text',
2314
                'pass_percentage',
2315
                [get_lang('Pass percentage'), null, '%'],
2316
                ['id' => 'pass_percentage']
2317
            );
2318
2319
            $form->addRule('pass_percentage', get_lang('Numerical'), 'numeric');
2320
            $form->addRule('pass_percentage', get_lang('Value is too small.'), 'min_numeric_length', 0);
2321
            $form->addRule('pass_percentage', get_lang('Value is too big.'), 'max_numeric_length', 100);
2322
2323
            // add the text_when_finished textbox
2324
            $form->addHtmlEditor(
2325
                'text_when_finished',
2326
                get_lang('Text appearing at the end of the test when the user has succeeded or if no pass percentage was set.'),
2327
                false,
2328
                false,
2329
                $editor_config
2330
            );
2331
            $form->addHtmlEditor(
2332
                'text_when_finished_failure',
2333
                get_lang('Text appearing at the end of the test when the user has failed.'),
2334
                false,
2335
                false,
2336
                $editor_config
2337
            );
2338
2339
            $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
2340
            if (true === $allow) {
2341
                $settings = ExerciseLib::getNotificationSettings();
2342
                $group = [];
2343
                foreach ($settings as $itemId => $label) {
2344
                    $group[] = $form->createElement(
2345
                        'checkbox',
2346
                        'notifications[]',
2347
                        null,
2348
                        $label,
2349
                        ['value' => $itemId]
2350
                    );
2351
                }
2352
                $form->addGroup($group, '', [get_lang('E-mail notifications')]);
2353
            }
2354
2355
            $form->addCheckBox('update_title_in_lps', null, get_lang('Update this title in learning paths'));
2356
2357
            $defaults = [];
2358
            if ('true' === api_get_setting('search_enabled')) {
2359
                $form->addCheckBox('index_document', '', get_lang('Index document text?'));
2360
                $form->addSelectLanguage('language', get_lang('Document language for indexation'));
2361
                $specific_fields = get_specific_field_list();
2362
2363
                foreach ($specific_fields as $specific_field) {
2364
                    $form->addElement('text', $specific_field['code'], $specific_field['name']);
2365
                    $filter = [
2366
                        'c_id' => api_get_course_int_id(),
2367
                        'field_id' => $specific_field['id'],
2368
                        'ref_id' => $this->getId(),
2369
                        'tool_id' => "'".TOOL_QUIZ."'",
2370
                    ];
2371
                    $values = get_specific_field_values_list($filter, ['value']);
2372
                    if (!empty($values)) {
2373
                        $arr_str_values = [];
2374
                        foreach ($values as $value) {
2375
                            $arr_str_values[] = $value['value'];
2376
                        }
2377
                        $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
2378
                    }
2379
                }
2380
            }
2381
2382
            $skillList = SkillModel::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
2383
2384
            $extraField = new ExtraField('exercise');
2385
            $extraField->addElements(
2386
                $form,
2387
                $this->iId,
2388
                ['notifications'], //exclude
2389
                false, // filter
2390
                false, // tag as select
2391
                [], //show only fields
2392
                [], // order fields
2393
                [] // extra data
2394
            );
2395
            $settings = api_get_configuration_value('exercise_finished_notification_settings');
2396
            if (!empty($settings)) {
2397
                $options = [];
2398
                foreach ($settings as $name => $data) {
2399
                    $options[$name] = $name;
2400
                }
2401
                $form->addSelect(
2402
                    'extra_notifications',
2403
                    get_lang('Notifications'),
2404
                    $options,
2405
                    ['placeholder' => get_lang('Please select an option')]
2406
                );
2407
            }
2408
            $form->addElement('html', '</div>'); //End advanced setting
2409
            $form->addElement('html', '</div>');
2410
        }
2411
2412
        // submit
2413
        if (isset($_GET['id'])) {
2414
            $form->addButtonSave(get_lang('Edit test name and settings'), 'submitExercise');
2415
        } else {
2416
            $form->addButtonUpdate(get_lang('Proceed to questions'), 'submitExercise');
2417
        }
2418
2419
        $form->addRule('exerciseTitle', get_lang('Name'), 'required');
2420
2421
        // defaults
2422
        if ('full' == $type) {
2423
            // rules
2424
            $form->addRule('exerciseAttempts', get_lang('Numerical'), 'numeric');
2425
            $form->addRule('start_time', get_lang('Invalid date'), 'datetime');
2426
            $form->addRule('end_time', get_lang('Invalid date'), 'datetime');
2427
2428
            if ($this->getId() > 0) {
2429
                $defaults['randomQuestions'] = $this->random;
2430
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2431
                $defaults['exerciseType'] = $this->selectType();
2432
                $defaults['exerciseTitle'] = $this->get_formated_title();
2433
                $defaults['exerciseDescription'] = $this->selectDescription();
2434
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2435
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2436
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2437
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2438
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2439
                $defaults['review_answers'] = $this->review_answers;
2440
                $defaults['randomByCat'] = $this->getRandomByCategory();
2441
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2442
                $defaults['text_when_finished_failure'] = $this->getTextWhenFinishedFailure();
2443
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2444
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2445
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2446
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2447
                $defaults['show_previous_button'] = $this->showPreviousButton();
2448
                $defaults['quiz_category_id'] = $this->getQuizCategoryId();
2449
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2450
                $defaults['hide_question_number'] = $this->getHideQuestionNumber();
2451
2452
                if (!empty($this->start_time)) {
2453
                    $defaults['activate_start_date_check'] = 1;
2454
                }
2455
                if (!empty($this->end_time)) {
2456
                    $defaults['activate_end_date_check'] = 1;
2457
                }
2458
2459
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date(
2460
                    'Y-m-d 12:00:00'
2461
                );
2462
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date(
2463
                    'Y-m-d 12:00:00',
2464
                    time() + 84600
2465
                );
2466
2467
                // Get expired time
2468
                if ('0' != $this->expired_time) {
2469
                    $defaults['enabletimercontrol'] = 1;
2470
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2471
                } else {
2472
                    $defaults['enabletimercontroltotalminutes'] = 0;
2473
                }
2474
                $defaults['skills'] = array_keys($skillList);
2475
                $defaults['notifications'] = $this->getNotifications();
2476
            } else {
2477
                $defaults['exerciseType'] = 2;
2478
                $defaults['exerciseAttempts'] = 0;
2479
                $defaults['randomQuestions'] = 0;
2480
                $defaults['randomAnswers'] = 0;
2481
                $defaults['exerciseDescription'] = '';
2482
                $defaults['exerciseFeedbackType'] = 0;
2483
                $defaults['results_disabled'] = 0;
2484
                $defaults['randomByCat'] = 0;
2485
                $defaults['text_when_finished'] = '';
2486
                $defaults['text_when_finished_failure'] = '';
2487
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2488
                $defaults['display_category_name'] = 1;
2489
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2490
                $defaults['pass_percentage'] = '';
2491
                $defaults['end_button'] = $this->selectEndButton();
2492
                $defaults['question_selection_type'] = 1;
2493
                $defaults['hide_question_title'] = 0;
2494
                $defaults['show_previous_button'] = 1;
2495
                $defaults['on_success_message'] = null;
2496
                $defaults['on_failed_message'] = null;
2497
            }
2498
        } else {
2499
            $defaults['exerciseTitle'] = $this->selectTitle();
2500
            $defaults['exerciseDescription'] = $this->selectDescription();
2501
        }
2502
2503
        if ('true' === api_get_setting('search_enabled')) {
2504
            $defaults['index_document'] = 'checked="checked"';
2505
        }
2506
2507
        $this->setPageResultConfigurationDefaults($defaults);
2508
        $form->setDefaults($defaults);
2509
2510
        // Freeze some elements.
2511
        if (0 != $this->getId() && false == $this->edit_exercise_in_lp) {
2512
            $elementsToFreeze = [
2513
                'randomQuestions',
2514
                //'randomByCat',
2515
                'exerciseAttempts',
2516
                'propagate_neg',
2517
                'enabletimercontrol',
2518
                'review_answers',
2519
            ];
2520
2521
            foreach ($elementsToFreeze as $elementName) {
2522
                /** @var HTML_QuickForm_element $element */
2523
                $element = $form->getElement($elementName);
2524
                $element->freeze();
2525
            }
2526
        }
2527
    }
2528
2529
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2530
    {
2531
        // Feedback type.
2532
        $feedback = [];
2533
        $warning = sprintf(
2534
            get_lang("The setting \"%s\" will change to \"%s\""),
2535
            get_lang('Show score to learner'),
2536
            get_lang('Auto-evaluation mode: show score and expected answers')
2537
        );
2538
        $endTest = $form->createElement(
2539
            'radio',
2540
            'exerciseFeedbackType',
2541
            null,
2542
            get_lang('At end of test'),
2543
            EXERCISE_FEEDBACK_TYPE_END,
2544
            [
2545
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2546
                //'onclick' => 'if confirm() check_feedback()',
2547
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_feedback(); } else { return false;} ',
2548
            ]
2549
        );
2550
2551
        $noFeedBack = $form->createElement(
2552
            'radio',
2553
            'exerciseFeedbackType',
2554
            null,
2555
            get_lang('Exam (no feedback)'),
2556
            EXERCISE_FEEDBACK_TYPE_EXAM,
2557
            [
2558
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2559
            ]
2560
        );
2561
2562
        $feedback[] = $endTest;
2563
        $feedback[] = $noFeedBack;
2564
2565
        $scenarioEnabled = 'true' === api_get_setting('enable_quiz_scenario');
2566
        $freeze = true;
2567
        if ($scenarioEnabled) {
2568
            if ($this->getQuestionCount() > 0) {
2569
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2570
                if (false === $hasDifferentQuestion) {
2571
                    $freeze = false;
2572
                }
2573
            } else {
2574
                $freeze = false;
2575
            }
2576
            // Can't convert a question from one feedback to another
2577
            $direct = $form->createElement(
2578
                'radio',
2579
                'exerciseFeedbackType',
2580
                null,
2581
                get_lang('Adaptative test with immediate feedback'),
2582
                EXERCISE_FEEDBACK_TYPE_DIRECT,
2583
                [
2584
                    'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2585
                    'onclick' => 'check_direct_feedback()',
2586
                ]
2587
            );
2588
2589
            $directPopUp = $form->createElement(
2590
                'radio',
2591
                'exerciseFeedbackType',
2592
                null,
2593
                get_lang('Direct pop-up mode'),
2594
                EXERCISE_FEEDBACK_TYPE_POPUP,
2595
                ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2596
            );
2597
            if ($freeze) {
2598
                $direct->freeze();
2599
                $directPopUp->freeze();
2600
            }
2601
2602
            // If has delineation freeze all.
2603
            $hasDelineation = $this->hasQuestionWithType(HOT_SPOT_DELINEATION);
2604
            if ($hasDelineation) {
2605
                $endTest->freeze();
2606
                $noFeedBack->freeze();
2607
                $direct->freeze();
2608
                $directPopUp->freeze();
2609
            }
2610
2611
            $feedback[] = $direct;
2612
            $feedback[] = $directPopUp;
2613
        }
2614
2615
        $form->addGroup(
2616
            $feedback,
2617
            null,
2618
            [
2619
                get_lang('Feedback'),
2620
                get_lang(
2621
                    'How should we show the feedback/comment for each question? This option defines how it will be shown to the learner when taking the test. We recommend you try different options by editing your test options before having learners take it.'
2622
                ),
2623
            ]
2624
        );
2625
    }
2626
2627
    /**
2628
     * function which process the creation of exercises.
2629
     *
2630
     * @param FormValidator $form
2631
     *
2632
     * @return int c_quiz.iid
2633
     */
2634
    public function processCreation($form)
2635
    {
2636
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2637
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2638
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2639
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2640
        $this->updateType($form->getSubmitValue('exerciseType'));
2641
2642
        // If direct feedback then force to One per page
2643
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2644
            $this->updateType(ONE_PER_PAGE);
2645
        }
2646
2647
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2648
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2649
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2650
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2651
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2652
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2653
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2654
        $this->setTextWhenFinished($form->getSubmitValue('text_when_finished'));
2655
        $this->setTextWhenFinishedFailure($form->getSubmitValue('text_when_finished_failure'));
2656
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2657
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2658
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2659
        $this->updateCategories($form->getSubmitValue('category'));
2660
        $this->updateEndButton($form->getSubmitValue('end_button'));
2661
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2662
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2663
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2664
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2665
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2666
        $this->setModelType($form->getSubmitValue('model_type'));
2667
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2668
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2669
        $this->sessionId = api_get_session_id();
2670
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2671
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2672
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2673
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2674
        $this->setNotifications($form->getSubmitValue('notifications'));
2675
        $this->setQuizCategoryId($form->getSubmitValue('quiz_category_id'));
2676
        $this->setPageResultConfiguration($form->getSubmitValues());
2677
        $this->setHideQuestionNumber($form->getSubmitValue('hide_question_number'));
2678
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
2679
2680
        $this->start_time = null;
2681
        if (1 == $form->getSubmitValue('activate_start_date_check')) {
2682
            $start_time = $form->getSubmitValue('start_time');
2683
            $this->start_time = api_get_utc_datetime($start_time);
2684
        }
2685
2686
        $this->end_time = null;
2687
        if (1 == $form->getSubmitValue('activate_end_date_check')) {
2688
            $end_time = $form->getSubmitValue('end_time');
2689
            $this->end_time = api_get_utc_datetime($end_time);
2690
        }
2691
2692
        $this->expired_time = 0;
2693
        if (1 == $form->getSubmitValue('enabletimercontrol')) {
2694
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
2695
            if (0 == $this->expired_time) {
2696
                $this->expired_time = $expired_total_time;
2697
            }
2698
        }
2699
2700
        $this->random_answers = 0;
2701
        if (1 == $form->getSubmitValue('randomAnswers')) {
2702
            $this->random_answers = 1;
2703
        }
2704
2705
        // Update title in all LPs that have this quiz added
2706
        if (1 == $form->getSubmitValue('update_title_in_lps')) {
2707
            $table = Database::get_course_table(TABLE_LP_ITEM);
2708
            $sql = "SELECT iid FROM $table
2709
                    WHERE
2710
                        item_type = 'quiz' AND
2711
                        path = '".$this->getId()."'
2712
                    ";
2713
            $result = Database::query($sql);
2714
            $items = Database::store_result($result);
2715
            if (!empty($items)) {
2716
                foreach ($items as $item) {
2717
                    $itemId = $item['iid'];
2718
                    $sql = "UPDATE $table
2719
                            SET title = '".$this->title."'
2720
                            WHERE iid = $itemId ";
2721
                    Database::query($sql);
2722
                }
2723
            }
2724
        }
2725
2726
        $iId = $this->save();
2727
        if (!empty($iId)) {
2728
            $values = $form->getSubmitValues();
2729
            $values['item_id'] = $iId;
2730
            $extraFieldValue = new ExtraFieldValue('exercise');
2731
            $extraFieldValue->saveFieldValues($values);
2732
2733
            SkillModel::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
2734
        }
2735
    }
2736
2737
    public function search_engine_save()
2738
    {
2739
        if (1 != $_POST['index_document']) {
2740
            return;
2741
        }
2742
        $course_id = api_get_course_id();
2743
        $specific_fields = get_specific_field_list();
2744
        $ic_slide = new IndexableChunk();
2745
2746
        $all_specific_terms = '';
2747
        foreach ($specific_fields as $specific_field) {
2748
            if (isset($_REQUEST[$specific_field['code']])) {
2749
                $sterms = trim($_REQUEST[$specific_field['code']]);
2750
                if (!empty($sterms)) {
2751
                    $all_specific_terms .= ' '.$sterms;
2752
                    $sterms = explode(',', $sterms);
2753
                    foreach ($sterms as $sterm) {
2754
                        $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2755
                        add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->getId(), $sterm);
2756
                    }
2757
                }
2758
            }
2759
        }
2760
2761
        // build the chunk to index
2762
        $ic_slide->addValue('title', $this->exercise);
2763
        $ic_slide->addCourseId($course_id);
2764
        $ic_slide->addToolId(TOOL_QUIZ);
2765
        $xapian_data = [
2766
            SE_COURSE_ID => $course_id,
2767
            SE_TOOL_ID => TOOL_QUIZ,
2768
            SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2769
            SE_USER => (int) api_get_user_id(),
2770
        ];
2771
        $ic_slide->xapian_data = serialize($xapian_data);
2772
        $exercise_description = $all_specific_terms.' '.$this->description;
2773
        $ic_slide->addValue('content', $exercise_description);
2774
2775
        $di = new ChamiloIndexer();
2776
        isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2777
        $di->connectDb(null, null, $lang);
2778
        $di->addChunk($ic_slide);
2779
2780
        //index and return search engine document id
2781
        $did = $di->index();
2782
        if ($did) {
2783
            // save it to db
2784
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2785
            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2786
			    VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2787
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2788
            Database::query($sql);
2789
        }
2790
    }
2791
2792
    public function search_engine_edit()
2793
    {
2794
        // update search enchine and its values table if enabled
2795
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2796
            $course_id = api_get_course_id();
2797
2798
            // actually, it consists on delete terms from db,
2799
            // insert new ones, create a new search engine document, and remove the old one
2800
            // get search_did
2801
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2802
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
2803
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2804
            $res = Database::query($sql);
2805
2806
            if (Database::num_rows($res) > 0) {
2807
                $se_ref = Database::fetch_array($res);
2808
                $specific_fields = get_specific_field_list();
2809
                $ic_slide = new IndexableChunk();
2810
2811
                $all_specific_terms = '';
2812
                foreach ($specific_fields as $specific_field) {
2813
                    delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->getId());
2814
                    if (isset($_REQUEST[$specific_field['code']])) {
2815
                        $sterms = trim($_REQUEST[$specific_field['code']]);
2816
                        $all_specific_terms .= ' '.$sterms;
2817
                        $sterms = explode(',', $sterms);
2818
                        foreach ($sterms as $sterm) {
2819
                            $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2820
                            add_specific_field_value(
2821
                                $specific_field['id'],
2822
                                $course_id,
2823
                                TOOL_QUIZ,
2824
                                $this->getId(),
2825
                                $sterm
2826
                            );
2827
                        }
2828
                    }
2829
                }
2830
2831
                // build the chunk to index
2832
                $ic_slide->addValue('title', $this->exercise);
2833
                $ic_slide->addCourseId($course_id);
2834
                $ic_slide->addToolId(TOOL_QUIZ);
2835
                $xapian_data = [
2836
                    SE_COURSE_ID => $course_id,
2837
                    SE_TOOL_ID => TOOL_QUIZ,
2838
                    SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2839
                    SE_USER => (int) api_get_user_id(),
2840
                ];
2841
                $ic_slide->xapian_data = serialize($xapian_data);
2842
                $exercise_description = $all_specific_terms.' '.$this->description;
2843
                $ic_slide->addValue('content', $exercise_description);
2844
2845
                $di = new ChamiloIndexer();
2846
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2847
                $di->connectDb(null, null, $lang);
2848
                $di->remove_document($se_ref['search_did']);
2849
                $di->addChunk($ic_slide);
2850
2851
                //index and return search engine document id
2852
                $did = $di->index();
2853
                if ($did) {
2854
                    // save it to db
2855
                    $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
2856
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2857
                    Database::query($sql);
2858
                    $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2859
                        VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2860
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2861
                    Database::query($sql);
2862
                }
2863
            } else {
2864
                $this->search_engine_save();
2865
            }
2866
        }
2867
    }
2868
2869
    public function search_engine_delete()
2870
    {
2871
        // remove from search engine if enabled
2872
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2873
            $course_id = api_get_course_id();
2874
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2875
            $sql = 'SELECT * FROM %s
2876
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2877
                    LIMIT 1';
2878
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2879
            $res = Database::query($sql);
2880
            if (Database::num_rows($res) > 0) {
2881
                $row = Database::fetch_array($res);
2882
                $di = new ChamiloIndexer();
2883
                $di->remove_document($row['search_did']);
2884
                unset($di);
2885
                $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2886
                foreach ($this->questionList as $question_i) {
2887
                    $sql = 'SELECT type FROM %s WHERE id=%s';
2888
                    $sql = sprintf($sql, $tbl_quiz_question, $question_i);
2889
                    $qres = Database::query($sql);
2890
                    if (Database::num_rows($qres) > 0) {
2891
                        $qrow = Database::fetch_array($qres);
2892
                        $objQuestion = Question::getInstance($qrow['type']);
2893
                        $objQuestion = Question::read((int) $question_i);
2894
                        $objQuestion->search_engine_edit($this->getId(), false, true);
2895
                        unset($objQuestion);
2896
                    }
2897
                }
2898
            }
2899
            $sql = 'DELETE FROM %s
2900
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2901
                    LIMIT 1';
2902
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2903
            Database::query($sql);
2904
2905
            // remove terms from db
2906
            delete_all_values_for_item($course_id, TOOL_QUIZ, $this->getId());
2907
        }
2908
    }
2909
2910
    public function selectExpiredTime()
2911
    {
2912
        return $this->expired_time;
2913
    }
2914
2915
    /**
2916
     * Cleans the student's results only for the Exercise tool (Not from the LP)
2917
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
2918
     * Works with exercises in sessions.
2919
     *
2920
     * @param bool   $cleanLpTests
2921
     * @param string $cleanResultBeforeDate
2922
     *
2923
     * @return int quantity of user's exercises deleted
2924
     */
2925
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
2926
    {
2927
        $sessionId = api_get_session_id();
2928
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2929
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
2930
2931
        $sql_where = '  AND
2932
                        orig_lp_id = 0 AND
2933
                        orig_lp_item_id = 0';
2934
2935
        // if we want to delete results from LP too
2936
        if ($cleanLpTests) {
2937
            $sql_where = '';
2938
        }
2939
2940
        // if we want to delete attempts before date $cleanResultBeforeDate
2941
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
2942
        if (!empty($cleanResultBeforeDate)) {
2943
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
2944
            if (api_is_valid_date($cleanResultBeforeDate)) {
2945
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
2946
            } else {
2947
                return 0;
2948
            }
2949
        }
2950
2951
        $sessionCondition = api_get_session_condition($sessionId);
2952
        $sql = "SELECT exe_id
2953
                FROM $table_track_e_exercises
2954
                WHERE
2955
                    c_id = ".api_get_course_int_id().' AND
2956
                    exe_exo_id = '.$this->getId()."
2957
                    $sessionCondition
2958
                    $sql_where";
2959
2960
        $result = Database::query($sql);
2961
        $exe_list = Database::store_result($result);
2962
2963
        // deleting TRACK_E_ATTEMPT table
2964
        // check if exe in learning path or not
2965
        $i = 0;
2966
        if (is_array($exe_list) && count($exe_list) > 0) {
2967
            foreach ($exe_list as $item) {
2968
                $sql = "DELETE FROM $table_track_e_attempt
2969
                        WHERE exe_id = '".$item['exe_id']."'";
2970
                Database::query($sql);
2971
                $i++;
2972
            }
2973
        }
2974
2975
        // delete TRACK_E_EXERCISES table
2976
        $sql = "DELETE FROM $table_track_e_exercises
2977
                WHERE
2978
                  c_id = ".api_get_course_int_id().' AND
2979
                  exe_exo_id = '.$this->getId()." $sql_where $sessionCondition";
2980
        Database::query($sql);
2981
2982
        $this->generateStats($this->getId(), api_get_course_info(), $sessionId);
2983
2984
        Event::addEvent(
2985
            LOG_EXERCISE_RESULT_DELETE,
2986
            LOG_EXERCISE_ID,
2987
            $this->getId(),
2988
            null,
2989
            null,
2990
            api_get_course_int_id(),
2991
            $sessionId
2992
        );
2993
2994
        return $i;
2995
    }
2996
2997
    /**
2998
     * Copies an exercise (duplicate all questions and answers).
2999
     */
3000
    public function copyExercise()
3001
    {
3002
        $exerciseObject = $this;
3003
        $categories = $exerciseObject->getCategoriesInExercise(true);
3004
        // Get all questions no matter the order/category settings
3005
        $questionList = $exerciseObject->getQuestionOrderedList();
3006
        $sourceId = $exerciseObject->iId;
3007
        // Force the creation of a new exercise
3008
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
3009
        $exerciseObject->iId = 0;
3010
        $exerciseObject->sessionId = api_get_session_id();
3011
        $courseId = api_get_course_int_id();
3012
        $exerciseObject->save();
3013
        $newId = $exerciseObject->getId();
3014
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
3015
3016
        $count = 1;
3017
        $batchSize = 20;
3018
        $em = Database::getManager();
3019
        if ($newId && !empty($questionList)) {
3020
            $extraField = new ExtraFieldValue('exercise');
3021
            $extraField->copy($sourceId, $newId);
3022
            // Question creation
3023
            foreach ($questionList as $oldQuestionId) {
3024
                $oldQuestionObj = Question::read($oldQuestionId, null, false);
3025
                $newQuestionId = $oldQuestionObj->duplicate();
3026
                if ($newQuestionId) {
3027
                    $newQuestionObj = Question::read($newQuestionId, null, false);
3028
                    if (isset($newQuestionObj) && $newQuestionObj) {
3029
                        $sql = "INSERT INTO $exerciseRelQuestionTable (question_id, quiz_id, question_order)
3030
                                VALUES (".$newQuestionId.", ".$newId.", '$count')";
3031
                        Database::query($sql);
3032
                        $count++;
3033
                        if (!empty($oldQuestionObj->category)) {
3034
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
3035
                        }
3036
3037
                        // This should be moved to the duplicate function
3038
                        $newAnswerObj = new Answer($oldQuestionId, $courseId, $exerciseObject);
3039
                        $newAnswerObj->read();
3040
                        $newAnswerObj->duplicate($newQuestionObj);
3041
                        if (($count % $batchSize) === 0) {
3042
                            $em->clear(); // Detaches all objects from Doctrine!
3043
                        }
3044
                    }
3045
                }
3046
            }
3047
            if (!empty($categories)) {
3048
                $newCategoryList = [];
3049
                foreach ($categories as $category) {
3050
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
3051
                }
3052
                $exerciseObject->saveCategoriesInExercise($newCategoryList);
3053
            }
3054
        }
3055
    }
3056
3057
    /**
3058
     * Changes the exercise status.
3059
     *
3060
     * @param string $status - exercise status
3061
     */
3062
    public function updateStatus($status)
3063
    {
3064
        $this->active = $status;
3065
    }
3066
3067
    /**
3068
     * @param int    $lp_id
3069
     * @param int    $lp_item_id
3070
     * @param int    $lp_item_view_id
3071
     * @param string $status
3072
     *
3073
     * @return array
3074
     */
3075
    public function get_stat_track_exercise_info(
3076
        $lp_id = 0,
3077
        $lp_item_id = 0,
3078
        $lp_item_view_id = 0,
3079
        $status = 'incomplete'
3080
    ) {
3081
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3082
        $lp_id = (int) $lp_id;
3083
        $lp_item_id = (int) $lp_item_id;
3084
        $lp_item_view_id = (int) $lp_item_view_id;
3085
3086
        $sessionCondition = api_get_session_condition(api_get_session_id());
3087
        $condition = " WHERE exe_exo_id 	= ".$this->getId()." AND
3088
					   exe_user_id 			= '".api_get_user_id()."' AND
3089
					   c_id                 = ".api_get_course_int_id()." AND
3090
					   status 				= '".Database::escape_string($status)."' AND
3091
					   orig_lp_id 			= $lp_id AND
3092
					   orig_lp_item_id 		= $lp_item_id AND
3093
                       orig_lp_item_view_id =  $lp_item_view_id
3094
					   ";
3095
3096
        $sql_track = " SELECT * FROM  $track_exercises $condition $sessionCondition LIMIT 1 ";
3097
3098
        $result = Database::query($sql_track);
3099
        $new_array = [];
3100
        if (Database::num_rows($result) > 0) {
3101
            $new_array = Database::fetch_assoc($result);
3102
            $new_array['num_exe'] = Database::num_rows($result);
3103
        }
3104
3105
        return $new_array;
3106
    }
3107
3108
    /**
3109
     * Saves a test attempt.
3110
     *
3111
     * @param int   $clock_expired_time   clock_expired_time
3112
     * @param int   $safe_lp_id           lp id
3113
     * @param int   $safe_lp_item_id      lp item id
3114
     * @param int   $safe_lp_item_view_id lp item_view id
3115
     * @param array $questionList
3116
     * @param float $weight
3117
     *
3118
     * @throws \Doctrine\ORM\ORMException
3119
     * @throws \Doctrine\ORM\OptimisticLockException
3120
     * @throws \Doctrine\ORM\TransactionRequiredException
3121
     *
3122
     * @return int
3123
     */
3124
    public function save_stat_track_exercise_info(
3125
        $clock_expired_time,
3126
        $safe_lp_id = 0,
3127
        $safe_lp_item_id = 0,
3128
        $safe_lp_item_view_id = 0,
3129
        $questionList = [],
3130
        $weight = 0
3131
    ) {
3132
        $safe_lp_id = (int) $safe_lp_id;
3133
        $safe_lp_item_id = (int) $safe_lp_item_id;
3134
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
3135
3136
        if (empty($clock_expired_time)) {
3137
            $clock_expired_time = null;
3138
        }
3139
3140
        $questionList = array_filter(
3141
            $questionList,
3142
            function (int $qid) {
3143
                $q = Question::read($qid);
3144
                return $q
3145
                    && !in_array(
3146
                        $q->type,
3147
                        [PAGE_BREAK, MEDIA_QUESTION],
3148
                        true
3149
                    );
3150
            }
3151
        );
3152
3153
        $questionList = array_map('intval', $questionList);
3154
        $em = Database::getManager();
3155
3156
        $quiz = $em->find(CQuiz::class, $this->getId());
3157
3158
        $trackExercise = (new TrackEExercise())
3159
            ->setSession(api_get_session_entity())
3160
            ->setCourse(api_get_course_entity())
3161
            ->setMaxScore($weight)
3162
            ->setDataTracking(implode(',', $questionList))
3163
            ->setUser(api_get_user_entity())
3164
            ->setUserIp(api_get_real_ip())
3165
            ->setOrigLpId($safe_lp_id)
3166
            ->setOrigLpItemId($safe_lp_item_id)
3167
            ->setOrigLpItemViewId($safe_lp_item_view_id)
3168
            ->setExpiredTimeControl($clock_expired_time)
3169
            ->setQuiz($quiz)
3170
        ;
3171
        $em->persist($trackExercise);
3172
        $em->flush();
3173
3174
        return $trackExercise->getExeId();
3175
    }
3176
3177
    /**
3178
     * @param int    $question_id
3179
     * @param int    $questionNum
3180
     * @param array  $questions_in_media
3181
     * @param string $currentAnswer
3182
     * @param array  $myRemindList
3183
     * @param bool   $showPreviousButton
3184
     *
3185
     * @return string
3186
     */
3187
    public function show_button(
3188
        $question_id,
3189
        $questionNum,
3190
        $questions_in_media = [],
3191
        $currentAnswer = '',
3192
        $myRemindList = [],
3193
        $showPreviousButton = true
3194
    ) {
3195
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3196
        $nbrQuestions = $this->countQuestionsInExercise();
3197
        $buttonList = [];
3198
        $html = $label = '';
3199
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3200
3201
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3202
            ONE_PER_PAGE == $this->type
3203
        ) {
3204
            $urlTitle = get_lang('Proceed with the test');
3205
            if ($questionNum == count($this->questionList)) {
3206
                $urlTitle = get_lang('End test');
3207
            }
3208
3209
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3210
            $url .= '&'.http_build_query(
3211
                    [
3212
                        'learnpath_id' => $safe_lp_id,
3213
                        'learnpath_item_id' => $safe_lp_item_id,
3214
                        'learnpath_item_view_id' => $safe_lp_item_view_id,
3215
                        'hotspot' => $hotspotGet,
3216
                        'nbrQuestions' => $nbrQuestions,
3217
                        'num' => $questionNum,
3218
                        'exerciseType' => $this->type,
3219
                        'exerciseId' => $this->getId(),
3220
                        'reminder' => empty($myRemindList) ? null : 2,
3221
                        'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3222
                    ]
3223
                );
3224
3225
            $params = [
3226
                'class' => 'ajax btn btn--plain no-close-button',
3227
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3228
                'data-size' => 'md',
3229
                'id' => "button_$question_id",
3230
            ];
3231
3232
            if (EXERCISE_FEEDBACK_TYPE_POPUP === $this->getFeedbackType()) {
3233
                //$params['data-block-div-after-closing'] = "question_div_$question_id";
3234
                $params['data-block-closing'] = 'true';
3235
                $params['class'] .= ' no-header ';
3236
            }
3237
3238
            $html .= Display::url($urlTitle, $url, $params);
3239
            $html .= '<br />';
3240
3241
            return $html;
3242
        }
3243
3244
        if (!api_is_allowed_to_session_edit()) {
3245
            return '';
3246
        }
3247
3248
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 == $_REQUEST['reminder'];
3249
3250
        // User
3251
        $endReminderValue = false;
3252
        if (!empty($myRemindList) && $isReviewingAnswers) {
3253
            $endValue = end($myRemindList);
3254
            if ($endValue == $question_id) {
3255
                $endReminderValue = true;
3256
            }
3257
        }
3258
        $endTest = false;
3259
        if (ALL_ON_ONE_PAGE == $this->type || $nbrQuestions == $questionNum || $endReminderValue) {
3260
            if ($this->review_answers) {
3261
                $label = get_lang('Review selected questions');
3262
                $class = 'btn btn--success';
3263
            } else {
3264
                $endTest = true;
3265
                $label = get_lang('End test');
3266
                $class = 'btn btn--warning';
3267
            }
3268
        } else {
3269
            $label = get_lang('Next question');
3270
            $class = 'btn btn--primary';
3271
        }
3272
        // used to select it with jquery
3273
        $class .= ' question-validate-btn';
3274
        if (ONE_PER_PAGE == $this->type) {
3275
            if (1 != $questionNum && $this->showPreviousButton()) {
3276
                $prev_question = $questionNum - 2;
3277
                $showPreview = true;
3278
                if (!empty($myRemindList) && $isReviewingAnswers) {
3279
                    $beforeId = null;
3280
                    for ($i = 0; $i < count($myRemindList); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
3281
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3282
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3283
3284
                            break;
3285
                        }
3286
                    }
3287
3288
                    if (empty($beforeId)) {
3289
                        $showPreview = false;
3290
                    } else {
3291
                        $num = 0;
3292
                        foreach ($this->questionList as $originalQuestionId) {
3293
                            if ($originalQuestionId == $beforeId) {
3294
                                break;
3295
                            }
3296
                            $num++;
3297
                        }
3298
                        $prev_question = $num;
3299
                    }
3300
                }
3301
3302
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3303
                    $buttonList[] = Display::button(
3304
                        'previous_question_and_save',
3305
                        get_lang('Previous question'),
3306
                        [
3307
                            'type' => 'button',
3308
                            'class' => 'btn btn--plain',
3309
                            'data-prev' => $prev_question,
3310
                            'data-question' => $question_id,
3311
                        ]
3312
                    );
3313
                }
3314
            }
3315
3316
            // Next question
3317
            if (!empty($questions_in_media)) {
3318
                $buttonList[] = Display::button(
3319
                    'save_question_list',
3320
                    $label,
3321
                    [
3322
                        'type' => 'button',
3323
                        'class' => $class,
3324
                        'data-list' => implode(',', $questions_in_media),
3325
                    ]
3326
                );
3327
            } else {
3328
                $attributes = ['type' => 'button', 'class' => $class, 'data-question' => $question_id];
3329
                $name = 'save_now';
3330
                if ($endTest && api_get_configuration_value('quiz_check_all_answers_before_end_test')) {
3331
                    $name = 'check_answers';
3332
                }
3333
                $buttonList[] = Display::button(
3334
                    $name,
3335
                    $label,
3336
                    $attributes
3337
                );
3338
            }
3339
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3340
3341
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3342
3343
            return $html;
3344
        }
3345
3346
        if ($this->review_answers) {
3347
            $all_label = get_lang('Review selected questions');
3348
            $class = 'btn btn--success';
3349
        } else {
3350
            $all_label = get_lang('End test');
3351
            $class = 'btn btn--warning';
3352
        }
3353
        // used to select it with jquery
3354
        $class .= ' question-validate-btn';
3355
        $buttonList[] = Display::button(
3356
            'validate_all',
3357
            $all_label,
3358
            ['type' => 'button', 'class' => $class]
3359
        );
3360
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3361
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3362
3363
        return $html;
3364
    }
3365
3366
    /**
3367
     * @param int    $timeLeft in seconds
3368
     * @param string $url
3369
     *
3370
     * @return string
3371
     */
3372
    public function showSimpleTimeControl($timeLeft, $url = '')
3373
    {
3374
        $timeLeft = (int) $timeLeft;
3375
3376
        return "<script>
3377
        (function ensureModalStyles() {
3378
            if (document.getElementById('ch-modal-styles')) return;
3379
            var css = `
3380
#clock_warning { display: none; }
3381
.ch-modal { position: fixed; inset: 0; z-index: 9999; display: grid; place-items: center; padding: 16px; }
3382
.ch-modal::before { content: ''; position: absolute; inset: 0; background: rgba(0,0,0,.3); }
3383
.ch-panel { position: relative; width: 100%; max-width: 520px; background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,.08); padding: 16px; color:#111827; font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif; }
3384
.ch-close { position:absolute; right: 10px; top:10px; cursor:pointer; color:#6b7280; }
3385
            `;
3386
            var s = document.createElement('style');
3387
            s.id = 'ch-modal-styles';
3388
            s.textContent = css;
3389
            document.head.appendChild(s);
3390
        })();
3391
3392
        function wrapClockWarning() {
3393
            var \$cw = \$('#clock_warning');
3394
            if (!\$cw.length) return \$cw;
3395
            // turn container into modal
3396
            \$cw.addClass('ch-modal');
3397
            // ensure inner panel
3398
            if (!\$cw.find('.ch-panel').length) {
3399
                \$cw.html('<div class=\"ch-panel\"><span class=\"mdi mdi-close ch-close\" title=\"Close\" aria-label=\"Close\"></span>' + \$cw.html() + '</div>');
3400
            }
3401
            // close button
3402
            \$cw.on('click', '.ch-close', function(){ closeClockWarning(); });
3403
            return \$cw;
3404
        }
3405
3406
        function openClockWarning() {
3407
            var \$cw = wrapClockWarning();
3408
            if (!\$cw || !\$cw.length) return;
3409
            \$cw.show();
3410
3411
            // countdown 5s -> redirect
3412
            \$('#counter_to_redirect').epiclock({
3413
                mode: \$.epiclock.modes.countdown,
3414
                offset: {seconds: 5},
3415
                format: 's'
3416
            }).bind('timer', function () {
3417
                window.location.href = '$url';
3418
            });
3419
        }
3420
3421
        function closeClockWarning() {
3422
            \$('#clock_warning').hide();
3423
            window.location.href = '$url';
3424
        }
3425
3426
        function onExpiredTimeExercise() {
3427
            \$('#wrapper-clock').hide();
3428
            \$('#expired-message-id').show();
3429
            \$('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3430
            openClockWarning();
3431
        }
3432
3433
        \$(function() {
3434
            var time_left = parseInt(".$timeLeft.");
3435
            \$('#exercise_clock_warning').epiclock({
3436
                mode: \$.epiclock.modes.countdown,
3437
                offset: {seconds: time_left},
3438
                format: 'x:i:s',
3439
                renderer: 'minute'
3440
            }).bind('timer', function () {
3441
                onExpiredTimeExercise();
3442
            });
3443
            \$('#submit_save').click(function () {});
3444
        });
3445
    </script>";
3446
    }
3447
3448
    /**
3449
     * So the time control will work.
3450
     *
3451
     * @param int    $timeLeft
3452
     * @param string $redirectToUrl
3453
     *
3454
     * @return string
3455
     */
3456
    public function showTimeControlJS($timeLeft, $redirectToUrl = '')
3457
    {
3458
        $timeLeft = (int) $timeLeft;
3459
        $script = 'redirectExerciseToResult();';
3460
        if (ALL_ON_ONE_PAGE == $this->type) {
3461
            $script = "save_now_all('validate');";
3462
        } elseif (ONE_PER_PAGE == $this->type) {
3463
            $script = 'window.quizTimeEnding = true; $(\'[name=\"save_now\"]\').trigger(\'click\');';
3464
        }
3465
3466
        $exerciseSubmitRedirect = '';
3467
        if (!empty($redirectToUrl)) {
3468
            $exerciseSubmitRedirect = "window.location = '$redirectToUrl'";
3469
        }
3470
3471
        return "<script>
3472
        (function ensureModalStyles() {
3473
            if (document.getElementById('ch-modal-styles')) return;
3474
            var css = `
3475
#clock_warning { display: none; }
3476
.ch-modal { position: fixed; inset: 0; z-index: 9999; display: grid; place-items: center; padding: 16px; }
3477
.ch-modal::before { content: ''; position: absolute; inset: 0; background: rgba(0,0,0,.3); }
3478
.ch-panel { position: relative; width: 100%; max-width: 520px; background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,.08); padding: 16px; color:#111827; font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif; }
3479
.ch-close { position:absolute; right: 10px; top:10px; cursor:pointer; color:#6b7280; }
3480
            `;
3481
            var s = document.createElement('style');
3482
            s.id = 'ch-modal-styles';
3483
            s.textContent = css;
3484
            document.head.appendChild(s);
3485
        })();
3486
3487
        function wrapClockWarning() {
3488
            var \$cw = \$('#clock_warning');
3489
            if (!\$cw.length) return \$cw;
3490
            \$cw.addClass('ch-modal');
3491
            if (!\$cw.find('.ch-panel').length) {
3492
                \$cw.html('<div class=\"ch-panel\"><span class=\"mdi mdi-close ch-close\" title=\"Close\" aria-label=\"Close\"></span>' + \$cw.html() + '</div>');
3493
            }
3494
            \$cw.on('click', '.ch-close', function(){ closeClockWarning(); });
3495
            return \$cw;
3496
        }
3497
3498
        function openClockWarning() {
3499
            var \$cw = wrapClockWarning();
3500
            if (!\$cw || !\$cw.length) return;
3501
            \$cw.show();
3502
3503
            \$('#counter_to_redirect').epiclock({
3504
                mode: \$.epiclock.modes.countdown,
3505
                offset: {seconds: 5},
3506
                format: 's'
3507
            }).bind('timer', function () {
3508
                send_form();
3509
            });
3510
        }
3511
3512
        function closeClockWarning() {
3513
            \$('#clock_warning').hide();
3514
            send_form();
3515
        }
3516
3517
        function send_form() {
3518
            if (\$('#exercise_form').length) {
3519
                $script
3520
            } else {
3521
                $exerciseSubmitRedirect
3522
                // In exercise_reminder.php
3523
                final_submit();
3524
            }
3525
        }
3526
3527
        function onExpiredTimeExercise() {
3528
            \$('#wrapper-clock').hide();
3529
            \$('#expired-message-id').show();
3530
            \$('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3531
            openClockWarning();
3532
        }
3533
3534
        \$(function() {
3535
            var time_left = parseInt(".$timeLeft.");
3536
            \$('#exercise_clock_warning').epiclock({
3537
                mode: \$.epiclock.modes.countdown,
3538
                offset: {seconds: time_left},
3539
                format: 'x:C:s',
3540
                renderer: 'minute'
3541
            }).bind('timer', function () {
3542
                onExpiredTimeExercise();
3543
            });
3544
            \$('#submit_save').click(function () {});
3545
        });
3546
    </script>";
3547
    }
3548
3549
    /**
3550
     * This function was originally found in the exercise_show.php.
3551
     *
3552
     * @param int    $exeId
3553
     * @param int    $questionId
3554
     * @param mixed  $choice                                    the user-selected option
3555
     * @param string $from                                      function is called from 'exercise_show' or
3556
     *                                                          'exercise_result'
3557
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3558
     *                                                          coordinates
3559
     * @param bool   $save_results                              save results in the DB or just show the response
3560
     * @param bool   $from_database                             gets information from DB or from the current selection
3561
     * @param bool   $show_result                               show results or not
3562
     * @param int    $propagate_neg
3563
     * @param array  $hotspot_delineation_result
3564
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3565
     * @param bool   $updateResults
3566
     * @param bool   $showHotSpotDelineationTable
3567
     * @param int    $questionDuration                          seconds
3568
     *
3569
     * @return string html code
3570
     *
3571
     * @todo    reduce parameters of this function
3572
     */
3573
    public function manage_answer(
3574
        $exeId,
3575
        $questionId,
3576
        $choice,
3577
        $from = 'exercise_show',
3578
        $exerciseResultCoordinates = [],
3579
        $save_results = true,
3580
        $from_database = false,
3581
        $show_result = true,
3582
        $propagate_neg = 0,
3583
        $hotspot_delineation_result = [],
3584
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3585
        $updateResults = false,
3586
        $showHotSpotDelineationTable = false,
3587
        $questionDuration = 0
3588
    ) {
3589
        $debug = false;
3590
        //needed in order to use in the exercise_attempt() for the time
3591
        global $learnpath_id, $learnpath_item_id;
3592
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3593
        $em = Database::getManager();
3594
        $feedback_type = $this->getFeedbackType();
3595
        $results_disabled = $this->selectResultsDisabled();
3596
        $questionDuration = (int) $questionDuration;
3597
        $hotspotsSavedEarly = false;
3598
3599
        if ($debug) {
3600
            error_log('<------ manage_answer ------> ');
3601
            error_log('exe_id: '.$exeId);
3602
            error_log('$from:  '.$from);
3603
            error_log('$save_results: '.(int) $save_results);
3604
            error_log('$from_database: '.(int) $from_database);
3605
            error_log('$show_result: '.(int) $show_result);
3606
            error_log('$propagate_neg: '.$propagate_neg);
3607
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3608
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3609
            error_log('$learnpath_id: '.$learnpath_id);
3610
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3611
            error_log('$choice: '.print_r($choice, 1));
3612
            error_log('-----------------------------');
3613
        }
3614
3615
        $final_overlap = 0;
3616
        $final_missing = 0;
3617
        $final_excess = 0;
3618
        $overlap_color = 0;
3619
        $missing_color = 0;
3620
        $excess_color = 0;
3621
        $threadhold1 = 0;
3622
        $threadhold2 = 0;
3623
        $threadhold3 = 0;
3624
        $arrques = null;
3625
        $arrans = null;
3626
        $studentChoice = null;
3627
        $expectedAnswer = '';
3628
        $calculatedChoice = '';
3629
        $calculatedStatus = '';
3630
        $questionId = (int) $questionId;
3631
        $exeId = (int) $exeId;
3632
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3633
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3634
        $studentChoiceDegree = null;
3635
        $listCorrectAnswers = [];
3636
3637
        // Creates a temporary Question object
3638
        $course_id = $this->course_id;
3639
        $objQuestionTmp = Question::read($questionId, $this->course);
3640
3641
        if (false === $objQuestionTmp) {
3642
            return false;
3643
        }
3644
3645
        $questionName = $objQuestionTmp->selectTitle();
3646
        $questionWeighting = $objQuestionTmp->selectWeighting();
3647
        $answerType = $objQuestionTmp->selectType();
3648
        $quesId = $objQuestionTmp->getId();
3649
        $extra = $objQuestionTmp->extra;
3650
        $next = 1; //not for now
3651
        $totalWeighting = 0;
3652
        $totalScore = 0;
3653
3654
        // Extra information of the question
3655
        if ((
3656
                MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
3657
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
3658
            )
3659
            && !empty($extra)
3660
        ) {
3661
            $extra = explode(':', $extra);
3662
            // Fixes problems with negatives values using intval
3663
            $true_score = (float) trim($extra[0]);
3664
            $false_score = (float) trim($extra[1]);
3665
            $doubt_score = (float) trim($extra[2]);
3666
        }
3667
3668
        // Construction of the Answer object
3669
        $objAnswerTmp = new Answer($questionId, $course_id);
3670
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3671
3672
        if ($debug) {
3673
            error_log('Count of possible answers: '.$nbrAnswers);
3674
            error_log('$answerType: '.$answerType);
3675
        }
3676
3677
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
3678
            $choiceTmp = $choice;
3679
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3680
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3681
        }
3682
3683
        if (FREE_ANSWER == $answerType ||
3684
            ORAL_EXPRESSION == $answerType ||
3685
            CALCULATED_ANSWER == $answerType ||
3686
            ANNOTATION == $answerType ||
3687
            UPLOAD_ANSWER == $answerType
3688
        ) {
3689
            $nbrAnswers = 1;
3690
        }
3691
3692
        $generatedFilesHtml = '';
3693
        if ($answerType == ORAL_EXPRESSION) {
3694
            $generatedFilesHtml = ExerciseLib::getOralFileAudio($exeId, $questionId);
3695
        }
3696
3697
        $user_answer = '';
3698
        // Get answer list for matching
3699
        $sql = "SELECT iid, answer
3700
                FROM $table_ans
3701
                WHERE question_id = $questionId";
3702
        $res_answer = Database::query($sql);
3703
3704
        $answerMatching = [];
3705
        while ($real_answer = Database::fetch_array($res_answer)) {
3706
            $answerMatching[$real_answer['iid']] = $real_answer['answer'];
3707
        }
3708
3709
        // Get first answer needed for global question, no matter the answer shuffle option;
3710
        $firstAnswer = [];
3711
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
3712
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
3713
        ) {
3714
            $sql = "SELECT *
3715
                    FROM $table_ans
3716
                    WHERE question_id = $questionId
3717
                    ORDER BY position
3718
                    LIMIT 1";
3719
            $result = Database::query($sql);
3720
            if (Database::num_rows($result)) {
3721
                $firstAnswer = Database::fetch_array($result);
3722
            }
3723
        }
3724
3725
        $real_answers = [];
3726
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
3727
3728
        $organs_at_risk_hit = 0;
3729
        $questionScore = 0;
3730
        $orderedHotSpots = [];
3731
        if (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, ANNOTATION], true)) {
3732
            $orderedHotSpots = $em->getRepository(TrackEHotspot::class)->findBy(
3733
                [
3734
                    'hotspotQuestionId' => $questionId,
3735
                    'course' => $course_id,
3736
                    'hotspotExeId' => $exeId,
3737
                ],
3738
                ['hotspotAnswerId' => 'ASC']
3739
            );
3740
        }
3741
3742
        if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION], true)) {
3743
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION == $answerType) {
3744
                $questionScore = $questionWeighting;
3745
            }
3746
            // Load student's selected choices
3747
            if ($from_database) {
3748
                $studentChoices = Database::store_result(
3749
                    Database::query(
3750
                        "SELECT answer FROM $TBL_TRACK_ATTEMPT WHERE exe_id = $exeId AND question_id = $questionId"
3751
                    ),
3752
                    'ASSOC'
3753
                );
3754
                $studentChoices = array_column($studentChoices, 'answer');
3755
            } else {
3756
                $studentChoices = array_values((array) $choice);
3757
            }
3758
3759
            // Compute the list of correct choice IDs
3760
            $correctChoices = array_keys(array_filter(
3761
                $answerMatching, // iid => answer text
3762
                function ($answerIdKey) use ($objAnswerTmp) {
3763
                    // Safeguards in case arrays are not initialized
3764
                    $iidList = is_array($objAnswerTmp->iid) ? $objAnswerTmp->iid : [];
3765
                    $pos = array_search($answerIdKey, $iidList, true);
3766
                    return $pos !== false && !empty($objAnswerTmp->correct[$pos]);
3767
                },
3768
                ARRAY_FILTER_USE_KEY
3769
            ));
3770
3771
            // Scoring
3772
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION === $answerType) {
3773
                // All-or-nothing scoring
3774
                $questionScore = (float) $questionWeighting;
3775
                if (array_diff($studentChoices, $correctChoices) || array_diff($correctChoices, $studentChoices)) {
3776
                    $questionScore = 0.0;
3777
                }
3778
            } else { // MULTIPLE_ANSWER_DROPDOWN
3779
                // Partial scoring using per-choice weighting
3780
                $questionScore = 0.0;
3781
                $iidList    = is_array($objAnswerTmp->iid) ? $objAnswerTmp->iid : [];
3782
                $correctArr = is_array($objAnswerTmp->correct) ? $objAnswerTmp->correct : [];
3783
                $weights    = is_array($objAnswerTmp->weighting) ? $objAnswerTmp->weighting : [];
3784
3785
                foreach ((array) $studentChoices as $choiceId) {
3786
                    $pos = array_search($choiceId, $iidList, true);
3787
                    if ($pos !== false && !empty($correctArr[$pos])) {
3788
                        $questionScore += (float) ($weights[$pos] ?? 0);
3789
                    }
3790
                }
3791
            }
3792
3793
            // Render (UI)
3794
            if ($show_result) {
3795
                echo ExerciseShowFunctions::displayMultipleAnswerDropdown(
3796
                    $this,
3797
                    $objAnswerTmp,
3798
                    $correctChoices,
3799
                    $studentChoices,
3800
                    $showTotalScoreAndUserChoicesInLastAttempt
3801
                );
3802
            }
3803
        }
3804
3805
        if ($debug) {
3806
            error_log('-- Start answer loop --');
3807
        }
3808
3809
        $answerDestination = null;
3810
        $userAnsweredQuestion = false;
3811
        $correctAnswerId = [];
3812
        $matchingCorrectAnswers = [];
3813
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3814
            $answer = $objAnswerTmp->selectAnswer($answerId);
3815
            $answerComment = $objAnswerTmp->selectComment($answerId);
3816
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3817
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3818
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3819
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3820
3821
            if ($debug) {
3822
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3823
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3824
                error_log("answerWeighting: $answerWeighting");
3825
            }
3826
3827
            // Delineation
3828
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3829
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3830
3831
            switch ($answerType) {
3832
                case UNIQUE_ANSWER:
3833
                case UNIQUE_ANSWER_IMAGE:
3834
                case UNIQUE_ANSWER_NO_OPTION:
3835
                case READING_COMPREHENSION:
3836
                    if ($from_database) {
3837
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3838
                                WHERE
3839
                                    exe_id = $exeId AND
3840
                                    question_id = $questionId";
3841
                        $result = Database::query($sql);
3842
                        $choice = Database::result($result, 0, 'answer');
3843
3844
                        if (false === $userAnsweredQuestion) {
3845
                            $userAnsweredQuestion = !empty($choice);
3846
                        }
3847
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3848
                        if ($studentChoice) {
3849
                            $questionScore += $answerWeighting;
3850
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3851
                            $correctAnswerId[] = $answerId;
3852
                        }
3853
                    } else {
3854
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3855
                        if ($studentChoice) {
3856
                            $questionScore += $answerWeighting;
3857
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3858
                            $correctAnswerId[] = $answerId;
3859
                        }
3860
                    }
3861
3862
                    break;
3863
                case MULTIPLE_ANSWER_TRUE_FALSE:
3864
                    if ($from_database) {
3865
                        $choice = [];
3866
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3867
                                WHERE
3868
                                    exe_id = $exeId AND
3869
                                    question_id = ".$questionId;
3870
3871
                        $result = Database::query($sql);
3872
                        while ($row = Database::fetch_array($result)) {
3873
                            $values = explode(':', $row['answer']);
3874
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3875
                            $option = isset($values[1]) ? $values[1] : '';
3876
                            $choice[$my_answer_id] = $option;
3877
                        }
3878
                        $userAnsweredQuestion = !empty($choice);
3879
                    }
3880
3881
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3882
                    if (!empty($studentChoice)) {
3883
                        $correctAnswerId[] = $answerAutoId;
3884
                        if ($studentChoice == $answerCorrect) {
3885
                            $questionScore += $true_score;
3886
                        } else {
3887
                            if (isset($quiz_question_options[$studentChoice])
3888
                                && in_array($quiz_question_options[$studentChoice]['name'], ["Don't know", 'DoubtScore'])
3889
                            ) {
3890
                                $questionScore += $doubt_score;
3891
                            } else {
3892
                                $questionScore += $false_score;
3893
                            }
3894
                        }
3895
                    } else {
3896
                        // If no result then the user just hit don't know
3897
                        $studentChoice = 3;
3898
                        $questionScore += $doubt_score;
3899
                    }
3900
                    $totalScore = $questionScore;
3901
3902
                    break;
3903
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3904
                    if ($from_database) {
3905
                        $choice = [];
3906
                        $choiceDegreeCertainty = [];
3907
                        $sql = "SELECT answer
3908
                                FROM $TBL_TRACK_ATTEMPT
3909
                                WHERE exe_id = $exeId AND question_id = $questionId";
3910
3911
                        $result = Database::query($sql);
3912
                        while ($row = Database::fetch_array($result)) {
3913
                            $ind = $row['answer'];
3914
                            $values = explode(':', $ind);
3915
                            $myAnswerId = $values[0] ?? null;
3916
                            $option = $values[1] ?? null;
3917
                            $percent = $values[2] ?? null;
3918
                            $choice[$myAnswerId] = $option;
3919
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3920
                        }
3921
                    }
3922
3923
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3924
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3925
3926
                    // student score update
3927
                    if (!empty($studentChoice)) {
3928
                        if ($studentChoice == $answerCorrect) {
3929
                            // correct answer and student is Unsure or PrettySur
3930
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3931
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3932
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3933
                            ) {
3934
                                $questionScore += $true_score;
3935
                            } else {
3936
                                // student ignore correct answer
3937
                                $questionScore += $doubt_score;
3938
                            }
3939
                        } else {
3940
                            // false answer and student is Unsure or PrettySur
3941
                            if (isset($quiz_question_options[$studentChoiceDegree]) && $quiz_question_options[$studentChoiceDegree]['position'] >= 3
3942
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3943
                                $questionScore += $false_score;
3944
                            } else {
3945
                                // student ignore correct answer
3946
                                $questionScore += $doubt_score;
3947
                            }
3948
                        }
3949
                    }
3950
                    $totalScore = $questionScore;
3951
3952
                    break;
3953
                case MULTIPLE_ANSWER:
3954
                case MULTIPLE_ANSWER_DROPDOWN:
3955
                    if ($from_database) {
3956
                        $choice = [];
3957
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3958
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3959
                        $resultans = Database::query($sql);
3960
                        while ($row = Database::fetch_array($resultans)) {
3961
                            $choice[$row['answer']] = 1;
3962
                        }
3963
3964
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3965
                        $real_answers[$answerId] = (bool) $studentChoice;
3966
3967
                        if ($studentChoice) {
3968
                            $questionScore += $answerWeighting;
3969
                        }
3970
                    } else {
3971
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3972
                        $real_answers[$answerId] = (bool) $studentChoice;
3973
3974
                        if (isset($studentChoice)
3975
                            || (MULTIPLE_ANSWER_DROPDOWN == $answerType && in_array($answerAutoId, $choice))
3976
                        ) {
3977
                            $correctAnswerId[] = $answerAutoId;
3978
                            $questionScore += $answerWeighting;
3979
                        }
3980
                    }
3981
                    $totalScore += $answerWeighting;
3982
3983
                    break;
3984
                case GLOBAL_MULTIPLE_ANSWER:
3985
                    if ($from_database) {
3986
                        $choice = [];
3987
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3988
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3989
                        $resultans = Database::query($sql);
3990
                        while ($row = Database::fetch_array($resultans)) {
3991
                            $choice[$row['answer']] = 1;
3992
                        }
3993
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3994
                        $real_answers[$answerId] = (bool) $studentChoice;
3995
                        if ($studentChoice) {
3996
                            $questionScore += $answerWeighting;
3997
                        }
3998
                    } else {
3999
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4000
                        if (isset($studentChoice)) {
4001
                            $questionScore += $answerWeighting;
4002
                        }
4003
                        $real_answers[$answerId] = (bool) $studentChoice;
4004
                    }
4005
                    $totalScore += $answerWeighting;
4006
                    if ($debug) {
4007
                        error_log("studentChoice: $studentChoice");
4008
                    }
4009
4010
                    break;
4011
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
4012
                    if ($from_database) {
4013
                        $choice = [];
4014
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4015
                                WHERE exe_id = $exeId AND question_id = $questionId";
4016
                        $resultans = Database::query($sql);
4017
                        while ($row = Database::fetch_array($resultans)) {
4018
                            $result = explode(':', $row['answer']);
4019
                            if (isset($result[0])) {
4020
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
4021
                                $option = isset($result[1]) ? $result[1] : '';
4022
                                $choice[$my_answer_id] = $option;
4023
                            }
4024
                        }
4025
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
4026
4027
                        $real_answers[$answerId] = false;
4028
                        if ($answerCorrect == $studentChoice) {
4029
                            $real_answers[$answerId] = true;
4030
                        }
4031
                    } else {
4032
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
4033
                        $real_answers[$answerId] = false;
4034
                        if ($answerCorrect == $studentChoice) {
4035
                            $real_answers[$answerId] = true;
4036
                        }
4037
                    }
4038
4039
                    break;
4040
                case MULTIPLE_ANSWER_COMBINATION:
4041
                    if ($from_database) {
4042
                        $choice = [];
4043
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4044
                                WHERE exe_id = $exeId AND question_id = $questionId";
4045
                        $resultans = Database::query($sql);
4046
                        while ($row = Database::fetch_array($resultans)) {
4047
                            $choice[$row['answer']] = 1;
4048
                        }
4049
4050
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4051
                        if (1 == $answerCorrect) {
4052
                            $real_answers[$answerId] = false;
4053
                            if ($studentChoice) {
4054
                                $real_answers[$answerId] = true;
4055
                            }
4056
                        } else {
4057
                            $real_answers[$answerId] = true;
4058
                            if ($studentChoice) {
4059
                                $real_answers[$answerId] = false;
4060
                            }
4061
                        }
4062
                    } else {
4063
                        $studentChoice = $choice[$answerAutoId] ?? null;
4064
                        if (1 == $answerCorrect) {
4065
                            $real_answers[$answerId] = false;
4066
                            if ($studentChoice) {
4067
                                $real_answers[$answerId] = true;
4068
                            }
4069
                        } else {
4070
                            $real_answers[$answerId] = true;
4071
                            if ($studentChoice) {
4072
                                $real_answers[$answerId] = false;
4073
                            }
4074
                        }
4075
                    }
4076
4077
                    break;
4078
                case FILL_IN_BLANKS:
4079
                case FILL_IN_BLANKS_COMBINATION:
4080
                    $str = '';
4081
                    $answerFromDatabase = '';
4082
                    if ($from_database) {
4083
                        $sql = "SELECT answer
4084
                                FROM $TBL_TRACK_ATTEMPT
4085
                                WHERE
4086
                                    exe_id = $exeId AND
4087
                                    question_id= $questionId ";
4088
                        $result = Database::query($sql);
4089
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
4090
                    }
4091
4092
                    // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
4093
                    if (false) {
4094
                        // the question is encoded like this
4095
                        // [A] B [C] D [E] F::10,10,10@1
4096
                        // number 1 before the "@" means that is a switchable fill in blank question
4097
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4098
                        // means that is a normal fill blank question
4099
                        // first we explode the "::"
4100
                        $pre_array = explode('::', $answer);
4101
4102
                        // is switchable fill blank or not
4103
                        $last = count($pre_array) - 1;
4104
                        $is_set_switchable = explode('@', $pre_array[$last]);
4105
                        $switchable_answer_set = false;
4106
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4107
                            $switchable_answer_set = true;
4108
                        }
4109
                        $answer = '';
4110
                        for ($k = 0; $k < $last; $k++) {
4111
                            $answer .= $pre_array[$k];
4112
                        }
4113
                        // splits weightings that are joined with a comma
4114
                        $answerWeighting = explode(',', $is_set_switchable[0]);
4115
                        // we save the answer because it will be modified
4116
                        $temp = $answer;
4117
                        $answer = '';
4118
                        $j = 0;
4119
                        //initialise answer tags
4120
                        $user_tags = $correct_tags = $real_text = [];
4121
                        // the loop will stop at the end of the text
4122
                        while (1) {
4123
                            // quits the loop if there are no more blanks (detect '[')
4124
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4125
                                // adds the end of the text
4126
                                $answer = $temp;
4127
                                $real_text[] = $answer;
4128
4129
                                break; //no more "blanks", quit the loop
4130
                            }
4131
                            // adds the piece of text that is before the blank
4132
                            //and ends with '[' into a general storage array
4133
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4134
                            $answer .= api_substr($temp, 0, $pos + 1);
4135
                            //take the string remaining (after the last "[" we found)
4136
                            $temp = api_substr($temp, $pos + 1);
4137
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4138
                            if (false === ($pos = api_strpos($temp, ']'))) {
4139
                                // adds the end of the text
4140
                                $answer .= $temp;
4141
4142
                                break;
4143
                            }
4144
                            if ($from_database) {
4145
                                $str = $answerFromDatabase;
4146
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4147
                                $str = str_replace('\r\n', '', $str);
4148
4149
                                $choice = $arr[1];
4150
                                if (isset($choice[$j])) {
4151
                                    $tmp = api_strrpos($choice[$j], ' / ');
4152
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4153
                                    $choice[$j] = trim($choice[$j]);
4154
                                    // Needed to let characters ' and " to work as part of an answer
4155
                                    $choice[$j] = stripslashes($choice[$j]);
4156
                                } else {
4157
                                    $choice[$j] = null;
4158
                                }
4159
                            } else {
4160
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4161
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4162
                            }
4163
4164
                            $user_tags[] = $choice[$j];
4165
                            // Put the contents of the [] answer tag into correct_tags[]
4166
                            $correct_tags[] = api_substr($temp, 0, $pos);
4167
                            $j++;
4168
                            $temp = api_substr($temp, $pos + 1);
4169
                        }
4170
                        $answer = '';
4171
                        $real_correct_tags = $correct_tags;
4172
                        $chosen_list = [];
4173
4174
                        for ($i = 0; $i < count($real_correct_tags); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4175
                            if (0 == $i) {
4176
                                $answer .= $real_text[0];
4177
                            }
4178
                            if (!$switchable_answer_set) {
4179
                                // Needed to parse ' and " characters
4180
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4181
                                if ($correct_tags[$i] == $user_tags[$i]) {
4182
                                    // gives the related weighting to the student
4183
                                    $questionScore += $answerWeighting[$i];
4184
                                    // increments total score
4185
                                    $totalScore += $answerWeighting[$i];
4186
                                    // adds the word in green at the end of the string
4187
                                    $answer .= $correct_tags[$i];
4188
                                } elseif (!empty($user_tags[$i])) {
4189
                                    // else if the word entered by the student IS NOT the same as
4190
                                    // the one defined by the professor
4191
                                    // adds the word in red at the end of the string, and strikes it
4192
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4193
                                } else {
4194
                                    // adds a tabulation if no word has been typed by the student
4195
                                    $answer .= ''; // remove &nbsp; that causes issue
4196
                                }
4197
                            } else {
4198
                                // switchable fill in the blanks
4199
                                if (in_array($user_tags[$i], $correct_tags)) {
4200
                                    $chosen_list[] = $user_tags[$i];
4201
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4202
                                    // gives the related weighting to the student
4203
                                    $questionScore += $answerWeighting[$i];
4204
                                    // increments total score
4205
                                    $totalScore += $answerWeighting[$i];
4206
                                    // adds the word in green at the end of the string
4207
                                    $answer .= $user_tags[$i];
4208
                                } elseif (!empty($user_tags[$i])) {
4209
                                    // else if the word entered by the student IS NOT the same
4210
                                    // as the one defined by the professor
4211
                                    // adds the word in red at the end of the string, and strikes it
4212
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4213
                                } else {
4214
                                    // adds a tabulation if no word has been typed by the student
4215
                                    $answer .= ''; // remove &nbsp; that causes issue
4216
                                }
4217
                            }
4218
4219
                            // adds the correct word, followed by ] to close the blank
4220
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4221
                            if (isset($real_text[$i + 1])) {
4222
                                $answer .= $real_text[$i + 1];
4223
                            }
4224
                        }
4225
                    } else {
4226
                        // insert the student result in the track_e_attempt table, field answer
4227
                        // $answer is the answer like in the c_quiz_answer table for the question
4228
                        // student data are choice[]
4229
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4230
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4231
                        $answerWeighting = $listCorrectAnswers['weighting'];
4232
                        // user choices is an array $choice
4233
4234
                        // get existing user data in n the BDD
4235
                        if ($from_database) {
4236
                            $listStudentResults = FillBlanks::getAnswerInfo(
4237
                                $answerFromDatabase,
4238
                                true
4239
                            );
4240
                            $choice = $listStudentResults['student_answer'];
4241
                        }
4242
4243
                        // loop other all blanks words
4244
                        if (!$switchableAnswerSet) {
4245
                            // not switchable answer, must be in the same place than teacher order
4246
                            for ($i = 0; $i < count($listCorrectAnswers['words']); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4247
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4248
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4249
4250
                                if ($debug) {
4251
                                    error_log("Student answer: $i");
4252
                                    error_log($studentAnswer);
4253
                                }
4254
4255
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4256
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4257
                                // ENT_QUOTES is used in order to transform ' to &#039;
4258
                                if (!$from_database) {
4259
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4260
                                    if ($debug) {
4261
                                        error_log('Student answer cleaned:');
4262
                                        error_log($studentAnswer);
4263
                                    }
4264
                                }
4265
4266
                                $isAnswerCorrect = 0;
4267
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4268
                                    // gives the related weighting to the student
4269
                                    $questionScore += $answerWeighting[$i];
4270
                                    // increments total score
4271
                                    $totalScore += $answerWeighting[$i];
4272
                                    $isAnswerCorrect = 1;
4273
                                }
4274
                                if ($debug) {
4275
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4276
                                }
4277
4278
                                $studentAnswerToShow = $studentAnswer;
4279
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4280
                                if ($debug) {
4281
                                    error_log("Fill in blank type: $type");
4282
                                }
4283
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4284
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4285
                                    if ('' != $studentAnswer) {
4286
                                        foreach ($listMenu as $item) {
4287
                                            if (sha1($item) == $studentAnswer) {
4288
                                                $studentAnswerToShow = $item;
4289
                                            }
4290
                                        }
4291
                                    }
4292
                                }
4293
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4294
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4295
                            }
4296
                        } else {
4297
                            // switchable answer
4298
                            $listStudentAnswerTemp = $choice;
4299
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4300
4301
                            // for every teacher answer, check if there is a student answer
4302
                            for ($i = 0; $i < count($listStudentAnswerTemp); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4303
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4304
                                $studentAnswerToShow = $studentAnswer;
4305
4306
                                if (empty($studentAnswer)) {
4307
                                    break;
4308
                                }
4309
4310
                                if ($debug) {
4311
                                    error_log("Student answer: $i");
4312
                                    error_log($studentAnswer);
4313
                                }
4314
4315
                                if (!$from_database) {
4316
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4317
                                    if ($debug) {
4318
                                        error_log("Student answer cleaned:");
4319
                                        error_log($studentAnswer);
4320
                                    }
4321
                                }
4322
4323
                                $found = false;
4324
                                for ($j = 0; $j < count($listTeacherAnswerTemp); $j++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4325
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4326
4327
                                    if (!$found) {
4328
                                        if (FillBlanks::isStudentAnswerGood(
4329
                                            $studentAnswer,
4330
                                            $correctAnswer,
4331
                                            $from_database
4332
                                        )) {
4333
                                            $questionScore += $answerWeighting[$i];
4334
                                            $totalScore += $answerWeighting[$i];
4335
                                            $listTeacherAnswerTemp[$j] = '';
4336
                                            $found = true;
4337
                                        }
4338
                                    }
4339
4340
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4341
                                    if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4342
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4343
                                        if (!empty($studentAnswer)) {
4344
                                            foreach ($listMenu as $key => $item) {
4345
                                                if (sha1($item) === $studentAnswer) {
4346
                                                    $studentAnswerToShow = $item;
4347
                                                    break;
4348
                                                }
4349
                                            }
4350
                                        }
4351
                                    }
4352
                                }
4353
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4354
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4355
                            }
4356
                        }
4357
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4358
                    }
4359
4360
                    break;
4361
                case CALCULATED_ANSWER:
4362
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4363
                    if (!empty($calculatedAnswerList)) {
4364
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4365
                        $preArray = explode('@@', $answer);
4366
                        $last = count($preArray) - 1;
4367
                        $answer = '';
4368
                        for ($k = 0; $k < $last; $k++) {
4369
                            $answer .= $preArray[$k];
4370
                        }
4371
                        $answerWeighting = [$answerWeighting];
4372
                        // we save the answer because it will be modified
4373
                        $temp = $answer;
4374
                        $answer = '';
4375
                        $j = 0;
4376
                        // initialise answer tags
4377
                        $userTags = $correctTags = $realText = [];
4378
                        // the loop will stop at the end of the text
4379
                        while (1) {
4380
                            // quits the loop if there are no more blanks (detect '[')
4381
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4382
                                // adds the end of the text
4383
                                $answer = $temp;
4384
                                $realText[] = $answer;
4385
4386
                                break; //no more "blanks", quit the loop
4387
                            }
4388
                            // adds the piece of text that is before the blank
4389
                            // and ends with '[' into a general storage array
4390
                            $realText[] = api_substr($temp, 0, $pos + 1);
4391
                            $answer .= api_substr($temp, 0, $pos + 1);
4392
                            // take the string remaining (after the last "[" we found)
4393
                            $temp = api_substr($temp, $pos + 1);
4394
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4395
                            if (false === ($pos = api_strpos($temp, ']'))) {
4396
                                // adds the end of the text
4397
                                $answer .= $temp;
4398
4399
                                break;
4400
                            }
4401
4402
                            if ($from_database) {
4403
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4404
                                        WHERE
4405
                                            exe_id = $exeId AND
4406
                                            question_id = $questionId ";
4407
                                $result = Database::query($sql);
4408
                                $str = Database::result($result, 0, 'answer');
4409
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4410
                                $str = str_replace('\r\n', '', $str);
4411
                                $choice = $arr[1];
4412
                                if (isset($choice[$j])) {
4413
                                    $tmp = api_strrpos($choice[$j], ' / ');
4414
                                    if ($tmp) {
4415
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4416
                                    } else {
4417
                                        $tmp = ltrim($tmp, '[');
4418
                                        $tmp = rtrim($tmp, ']');
4419
                                    }
4420
                                    $choice[$j] = trim($choice[$j]);
4421
                                    // Needed to let characters ' and " to work as part of an answer
4422
                                    $choice[$j] = stripslashes($choice[$j]);
4423
                                } else {
4424
                                    $choice[$j] = null;
4425
                                }
4426
                            } else {
4427
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4428
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4429
                            }
4430
                            $userTags[] = $choice[$j];
4431
                            // put the contents of the [] answer tag into correct_tags[]
4432
                            $correctTags[] = api_substr($temp, 0, $pos);
4433
                            $j++;
4434
                            $temp = api_substr($temp, $pos + 1);
4435
                        }
4436
                        $answer = '';
4437
                        $realCorrectTags = $correctTags;
4438
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4439
                        $expectedAnswer = '';
4440
                        $calculatedChoice = '';
4441
4442
                        for ($i = 0; $i < count($realCorrectTags); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4443
                            if (0 == $i) {
4444
                                $answer .= $realText[0];
4445
                            }
4446
                            // Needed to parse ' and " characters
4447
                            $userTags[$i] = stripslashes($userTags[$i]);
4448
                            if ($correctTags[$i] == $userTags[$i]) {
4449
                                // gives the related weighting to the student
4450
                                $questionScore += $answerWeighting[$i];
4451
                                // increments total score
4452
                                $totalScore += $answerWeighting[$i];
4453
                                // adds the word in green at the end of the string
4454
                                $answer .= $correctTags[$i];
4455
                                $calculatedChoice = $correctTags[$i];
4456
                            } elseif (!empty($userTags[$i])) {
4457
                                // else if the word entered by the student IS NOT the same as
4458
                                // the one defined by the professor
4459
                                // adds the word in red at the end of the string, and strikes it
4460
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4461
                                $calculatedChoice = $userTags[$i];
4462
                            } else {
4463
                                // adds a tabulation if no word has been typed by the student
4464
                                $answer .= ''; // remove &nbsp; that causes issue
4465
                            }
4466
                            // adds the correct word, followed by ] to close the blank
4467
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4468
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4469
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4470
                                $expectedAnswer = $realCorrectTags[$i];
4471
                            }
4472
                            $answer .= ']';
4473
                            if (isset($realText[$i + 1])) {
4474
                                $answer .= $realText[$i + 1];
4475
                            }
4476
                        }
4477
                    } else {
4478
                        if ($from_database) {
4479
                            $sql = "SELECT *
4480
                                    FROM $TBL_TRACK_ATTEMPT
4481
                                    WHERE
4482
                                        exe_id = $exeId AND
4483
                                        question_id = $questionId ";
4484
                            $result = Database::query($sql);
4485
                            $resultData = Database::fetch_assoc($result);
4486
                            $answer = $resultData['answer'];
4487
                            $questionScore = $resultData['marks'];
4488
                        }
4489
                    }
4490
4491
                    break;
4492
                case UPLOAD_ANSWER:
4493
                case FREE_ANSWER:
4494
                    if ($from_database) {
4495
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4496
                                 WHERE
4497
                                    exe_id = $exeId AND
4498
                                    question_id= ".$questionId;
4499
                        $result = Database::query($sql);
4500
                        $data = Database::fetch_array($result);
4501
                        $choice = '';
4502
                        $questionScore = 0;
4503
                        if ($data) {
4504
                            $choice = $data['answer'];
4505
                            $questionScore = $data['marks'];
4506
                        }
4507
4508
                        $choice = str_replace('\r\n', '', $choice);
4509
                        $choice = stripslashes($choice);
4510
4511
                        if (-1 == $questionScore) {
4512
                            $totalScore += 0;
4513
                        } else {
4514
                            $totalScore += $questionScore;
4515
                        }
4516
                        if ('' == $questionScore) {
4517
                            $questionScore = 0;
4518
                        }
4519
                        $arrques = $questionName;
4520
                        $arrans = $choice;
4521
                    } else {
4522
                        $studentChoice = $choice;
4523
                        if ($studentChoice) {
4524
                            //Fixing negative puntation see #2193
4525
                            $questionScore = 0;
4526
                            $totalScore += 0;
4527
                        }
4528
                    }
4529
4530
                    break;
4531
                case ORAL_EXPRESSION:
4532
                    if ($from_database) {
4533
                        $query = "SELECT answer, marks
4534
                                  FROM $TBL_TRACK_ATTEMPT
4535
                                  WHERE
4536
                                        exe_id = $exeId AND
4537
                                        question_id = $questionId
4538
                                 ";
4539
                        $resq = Database::query($query);
4540
                        $row = Database::fetch_assoc($resq);
4541
                        $choice = '';
4542
                        $questionScore = 0;
4543
4544
                        if (is_array($row)) {
4545
                            $choice = $row['answer'];
4546
                            $choice = str_replace('\r\n', '', $choice);
4547
                            $choice = stripslashes($choice);
4548
                            $questionScore = $row['marks'];
4549
                        }
4550
4551
                        if (-1 == $questionScore) {
4552
                            $totalScore += 0;
4553
                        } else {
4554
                            $totalScore += $questionScore;
4555
                        }
4556
                        $arrques = $questionName;
4557
                        $arrans = $choice;
4558
                    } else {
4559
                        $studentChoice = $choice;
4560
                        if ($studentChoice) {
4561
                            //Fixing negative puntation see #2193
4562
                            $questionScore = 0;
4563
                            $totalScore += 0;
4564
                        }
4565
                    }
4566
4567
                    break;
4568
                case MULTIPLE_ANSWER_DROPDOWN:
4569
                    $totalScore += $answerWeighting;
4570
                    break;
4571
                case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
4572
                    break;
4573
                case MATCHING_COMBINATION:
4574
                case MATCHING_DRAGGABLE_COMBINATION:
4575
                case DRAGGABLE:
4576
                case MATCHING_DRAGGABLE:
4577
                case MATCHING:
4578
                    if ($from_database) {
4579
                        $sql = "SELECT iid, answer
4580
                                FROM $table_ans
4581
                                WHERE
4582
                                    question_id = $questionId AND
4583
                                    correct = 0
4584
                                ";
4585
                        $result = Database::query($sql);
4586
                        // Getting the real answer
4587
                        $real_list = [];
4588
                        while ($realAnswer = Database::fetch_array($result)) {
4589
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4590
                        }
4591
4592
                        $orderBy = ' ORDER BY iid ';
4593
                        if (DRAGGABLE == $answerType) {
4594
                            $orderBy = ' ORDER BY correct ';
4595
                        }
4596
4597
                        $sql = "SELECT iid, answer, correct, ponderation
4598
                                FROM $table_ans
4599
                                WHERE
4600
                                    question_id = $questionId AND
4601
                                    correct <> 0
4602
                                $orderBy";
4603
                        $result = Database::query($sql);
4604
                        $options = [];
4605
                        $correctAnswers = [];
4606
                        while ($row = Database::fetch_assoc($result)) {
4607
                            $options[] = $row;
4608
                            $correctAnswers[$row['correct']] = $row['answer'];
4609
                        }
4610
4611
                        $questionScore = 0;
4612
                        $counterAnswer = 1;
4613
                        foreach ($options as $a_answers) {
4614
                            $i_answer_id = $a_answers['iid']; //3
4615
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4616
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4617
                            $i_answer_id_auto = $a_answers['iid']; // 3 - 4
4618
4619
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4620
                                    WHERE
4621
                                        exe_id = '$exeId' AND
4622
                                        question_id = '$questionId' AND
4623
                                        position = '$i_answer_id_auto'";
4624
                            $result = Database::query($sql);
4625
                            $s_user_answer = 0;
4626
                            if (Database::num_rows($result) > 0) {
4627
                                //  rich - good looking
4628
                                $s_user_answer = Database::result($result, 0, 0);
4629
                            }
4630
                            $i_answerWeighting = $a_answers['ponderation'];
4631
                            $user_answer = '';
4632
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4633
4634
                            if (!empty($s_user_answer)) {
4635
                                if (DRAGGABLE == $answerType) {
4636
                                    if ($s_user_answer == $i_answer_correct_answer) {
4637
                                        $questionScore += $i_answerWeighting;
4638
                                        $totalScore += $i_answerWeighting;
4639
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4640
                                        if ($this->showExpectedChoice() && !empty($i_answer_id_auto)) {
4641
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4642
                                        }
4643
                                        $status = Display::label(get_lang('Correct'), 'success');
4644
                                    } else {
4645
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4646
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4647
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4648
                                            $user_answer = $data['answer'];*/
4649
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4650
                                        }
4651
                                    }
4652
                                } else {
4653
                                    if ($s_user_answer == $i_answer_correct_answer) {
4654
                                        $questionScore += $i_answerWeighting;
4655
                                        $totalScore += $i_answerWeighting;
4656
                                        $status = Display::label(get_lang('Correct'), 'success');
4657
4658
                                        // Try with id
4659
                                        if (isset($real_list[$i_answer_id])) {
4660
                                            $user_answer = Display::span(
4661
                                                $real_list[$i_answer_id],
4662
                                                ['style' => 'color: #008000; font-weight: bold;']
4663
                                            );
4664
                                        }
4665
4666
                                        // Try with $i_answer_id_auto
4667
                                        if (empty($user_answer)) {
4668
                                            if (isset($real_list[$i_answer_id_auto])) {
4669
                                                $user_answer = Display::span(
4670
                                                    $real_list[$i_answer_id_auto],
4671
                                                    ['style' => 'color: #008000; font-weight: bold;']
4672
                                                );
4673
                                            }
4674
                                        }
4675
4676
                                        if (isset($real_list[$i_answer_correct_answer])) {
4677
                                            $matchingCorrectAnswers[$questionId]['from_database']['correct'][$i_answer_correct_answer] = $real_list[$i_answer_correct_answer];
4678
                                            $user_answer = Display::span(
4679
                                                $real_list[$i_answer_correct_answer],
4680
                                                ['style' => 'color: #008000; font-weight: bold;']
4681
                                            );
4682
                                        }
4683
                                    } else {
4684
                                        $user_answer = Display::span(
4685
                                            $real_list[$s_user_answer],
4686
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4687
                                        );
4688
                                        if ($this->showExpectedChoice()) {
4689
                                            if (isset($real_list[$s_user_answer])) {
4690
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4691
                                            }
4692
                                        }
4693
                                    }
4694
                                }
4695
                            } elseif (DRAGGABLE == $answerType) {
4696
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4697
                                if ($this->showExpectedChoice()) {
4698
                                    $user_answer = '';
4699
                                }
4700
                            } else {
4701
                                $user_answer = Display::span(
4702
                                    get_lang('Incorrect').' &nbsp;',
4703
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4704
                                );
4705
                                if ($this->showExpectedChoice()) {
4706
                                    $user_answer = '';
4707
                                }
4708
                            }
4709
4710
                            if ($show_result) {
4711
                                if (false === $this->showExpectedChoice() &&
4712
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4713
                                ) {
4714
                                    $user_answer = '';
4715
                                }
4716
                                switch ($answerType) {
4717
                                    case MATCHING:
4718
                                    case MATCHING_DRAGGABLE:
4719
                                    case MATCHING_COMBINATION:
4720
                                    case MATCHING_DRAGGABLE_COMBINATION:
4721
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4722
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4723
                                                break;
4724
                                            }
4725
                                        }
4726
                                        echo '<tr>';
4727
                                        if (!in_array(
4728
                                            $this->results_disabled,
4729
                                            [
4730
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4731
                                            ]
4732
                                        )
4733
                                        ) {
4734
                                            echo '<td>'.$s_answer_label.'</td>';
4735
                                            echo '<td>'.$user_answer.'</td>';
4736
                                        } else {
4737
                                            echo '<td>'.$s_answer_label.'</td>';
4738
                                            $status = Display::label(get_lang('Correct'), 'success');
4739
                                        }
4740
4741
                                        if ($this->showExpectedChoice()) {
4742
                                            if ($this->showExpectedChoiceColumn()) {
4743
                                                echo '<td>';
4744
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4745
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4746
                                                        true == $showTotalScoreAndUserChoicesInLastAttempt
4747
                                                    ) {
4748
                                                        echo Display::span(
4749
                                                            $real_list[$i_answer_correct_answer]
4750
                                                        );
4751
                                                    }
4752
                                                }
4753
                                                echo '</td>';
4754
                                            }
4755
                                            echo '<td class="text-center">'.$status.'</td>';
4756
                                        } else {
4757
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4758
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4759
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4760
                                                ) {
4761
                                                    if ($this->showExpectedChoiceColumn()) {
4762
                                                        echo '<td>';
4763
                                                        echo Display::span(
4764
                                                            $real_list[$i_answer_correct_answer],
4765
                                                            ['style' => 'color: #008000; font-weight: bold;']
4766
                                                        );
4767
                                                        echo '</td>';
4768
                                                    }
4769
                                                }
4770
                                            }
4771
                                        }
4772
                                        echo '</tr>';
4773
4774
                                        break;
4775
                                    case DRAGGABLE:
4776
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4777
                                            $s_answer_label = '';
4778
                                        }
4779
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4780
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4781
                                                break;
4782
                                            }
4783
                                        }
4784
                                        echo '<tr>';
4785
                                        if ($this->showExpectedChoice()) {
4786
                                            if (!in_array(
4787
                                                $this->results_disabled,
4788
                                                [
4789
                                                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4790
                                                    //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4791
                                                ]
4792
                                            )
4793
                                            ) {
4794
                                                echo '<td>'.$user_answer.'</td>';
4795
                                            } else {
4796
                                                $status = Display::label(get_lang('Correct'), 'success');
4797
                                            }
4798
                                            echo '<td>'.$s_answer_label.'</td>';
4799
                                            echo '<td class="text-center">'.$status.'</td>';
4800
                                        } else {
4801
                                            echo '<td>'.$s_answer_label.'</td>';
4802
                                            echo '<td>'.$user_answer.'</td>';
4803
                                            echo '<td>';
4804
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4805
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4806
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4807
                                                ) {
4808
                                                    echo Display::span(
4809
                                                        $real_list[$i_answer_correct_answer],
4810
                                                        ['style' => 'color: #008000; font-weight: bold;']
4811
                                                    );
4812
                                                }
4813
                                            }
4814
                                            echo '</td>';
4815
                                        }
4816
                                        echo '</tr>';
4817
4818
                                        break;
4819
                                }
4820
                            }
4821
                            $counterAnswer++;
4822
                        }
4823
                        $matchingCorrectAnswers[$questionId]['from_database']['count_options'] = count($options);
4824
                        break 2; // break the switch and the "for" condition
4825
                    } else {
4826
                        if ($answerCorrect) {
4827
                            if (isset($choice[$answerAutoId]) &&
4828
                                $answerCorrect == $choice[$answerAutoId]
4829
                            ) {
4830
                                $matchingCorrectAnswers[$questionId]['form_values']['correct'][$answerAutoId] = $choice[$answerAutoId];
4831
                                $correctAnswerId[] = $answerAutoId;
4832
                                $questionScore += $answerWeighting;
4833
                                $totalScore += $answerWeighting;
4834
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4835
                            } else {
4836
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4837
                                    $user_answer = Display::span(
4838
                                        $answerMatching[$choice[$answerAutoId]],
4839
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4840
                                    );
4841
                                }
4842
                            }
4843
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4844
                        }
4845
                        $matchingCorrectAnswers[$questionId]['form_values']['count_options'] = count($choice);
4846
                    }
4847
4848
                    break;
4849
                case HOT_SPOT:
4850
                case HOT_SPOT_COMBINATION:
4851
                    if ($from_database) {
4852
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4853
                        // Check auto id
4854
                        $foundAnswerId = $answerAutoId;
4855
                        $sql = "SELECT hotspot_correct
4856
                                FROM $TBL_TRACK_HOTSPOT
4857
                                WHERE
4858
                                    hotspot_exe_id = $exeId AND
4859
                                    hotspot_question_id= $questionId AND
4860
                                    hotspot_answer_id = $answerAutoId
4861
                                ORDER BY hotspot_id ASC";
4862
                        $result = Database::query($sql);
4863
                        if (Database::num_rows($result)) {
4864
                            $studentChoice = Database::result(
4865
                                $result,
4866
                                0,
4867
                                'hotspot_correct'
4868
                            );
4869
4870
                            if ($studentChoice) {
4871
                                $questionScore += $answerWeighting;
4872
                                $totalScore += $answerWeighting;
4873
                            }
4874
                        } else {
4875
                            // If answer.id is different:
4876
                            $sql = "SELECT hotspot_correct
4877
                                FROM $TBL_TRACK_HOTSPOT
4878
                                WHERE
4879
                                    hotspot_exe_id = $exeId AND
4880
                                    hotspot_question_id= $questionId AND
4881
                                    hotspot_answer_id = ".(int) $answerId.'
4882
                                ORDER BY hotspot_id ASC';
4883
                            $result = Database::query($sql);
4884
4885
                            $foundAnswerId = $answerId;
4886
                            if (Database::num_rows($result)) {
4887
                                $studentChoice = Database::result(
4888
                                    $result,
4889
                                    0,
4890
                                    'hotspot_correct'
4891
                                );
4892
4893
                                if ($studentChoice) {
4894
                                    $questionScore += $answerWeighting;
4895
                                    $totalScore += $answerWeighting;
4896
                                }
4897
                            } else {
4898
                                // check answer.iid
4899
                                if (!empty($answerIid)) {
4900
                                    $sql = "SELECT hotspot_correct
4901
                                            FROM $TBL_TRACK_HOTSPOT
4902
                                            WHERE
4903
                                                hotspot_exe_id = $exeId AND
4904
                                                hotspot_question_id= $questionId AND
4905
                                                hotspot_answer_id = $answerIid
4906
                                            ORDER BY hotspot_id ASC";
4907
                                    $result = Database::query($sql);
4908
4909
                                    $foundAnswerId = $answerIid;
4910
                                    $studentChoice = Database::result(
4911
                                        $result,
4912
                                        0,
4913
                                        'hotspot_correct'
4914
                                    );
4915
4916
                                    if ($studentChoice) {
4917
                                        $questionScore += $answerWeighting;
4918
                                        $totalScore += $answerWeighting;
4919
                                    }
4920
                                }
4921
                            }
4922
                        }
4923
                    } else {
4924
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4925
                            $choice[$answerAutoId] = 0;
4926
                            $choice[$answerIid] = 0;
4927
                        } else {
4928
                            $studentChoice = $choice[$answerAutoId];
4929
                            if (empty($studentChoice)) {
4930
                                $studentChoice = $choice[$answerIid];
4931
                            }
4932
                            $choiceIsValid = false;
4933
                            if (!empty($studentChoice)) {
4934
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4935
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4936
                                $choicePoint = Geometry::decodePoint($studentChoice);
4937
4938
                                switch ($hotspotType) {
4939
                                    case 'square':
4940
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4941
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4942
4943
                                        break;
4944
                                    case 'circle':
4945
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4946
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4947
4948
                                        break;
4949
                                    case 'poly':
4950
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4951
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4952
4953
                                        break;
4954
                                }
4955
                            }
4956
4957
                            $choice[$answerAutoId] = 0;
4958
                            if ($choiceIsValid) {
4959
                                $questionScore += $answerWeighting;
4960
                                $totalScore += $answerWeighting;
4961
                                $choice[$answerAutoId] = 1;
4962
                                $choice[$answerIid] = 1;
4963
                            }
4964
                            $studentChoice = $choiceIsValid ? 1 : 0;
4965
                        }
4966
                    }
4967
4968
                    break;
4969
                case HOT_SPOT_ORDER:
4970
                    // @todo never added to chamilo
4971
                    // for hotspot with fixed order
4972
                    $studentChoice = $choice['order'][$answerId];
4973
                    if ($studentChoice == $answerId) {
4974
                        $questionScore += $answerWeighting;
4975
                        $totalScore += $answerWeighting;
4976
                        $studentChoice = true;
4977
                    } else {
4978
                        $studentChoice = false;
4979
                    }
4980
4981
                    break;
4982
                case HOT_SPOT_DELINEATION:
4983
                    // for hotspot with delineation
4984
                    if ($from_database) {
4985
                        // getting the user answer
4986
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4987
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4988
                                    FROM $TBL_TRACK_HOTSPOT
4989
                                    WHERE
4990
                                        hotspot_exe_id = $exeId AND
4991
                                        hotspot_question_id= $questionId AND
4992
                                        hotspot_answer_id = '1'";
4993
                        // By default we take 1 because it's a delineation
4994
                        $resq = Database::query($query);
4995
                        $row = Database::fetch_assoc($resq);
4996
4997
                        if ($row && isset($row['hotspot_correct'], $row['hotspot_coordinate'])) {
4998
                            $choice = $row['hotspot_correct'];
4999
                            $user_answer = $row['hotspot_coordinate'];
5000
                            $coords = explode('/', $user_answer);
5001
                        } else {
5002
                            $choice = '';
5003
                            $user_answer = '';
5004
                            $coords = [];
5005
                        }
5006
5007
                        $user_array = '';
5008
                        foreach ($coords as $coord) {
5009
                            [$x, $y] = explode(';', $coord);
5010
                            $user_array .= round($x).';'.round($y).'/';
5011
                        }
5012
                        $user_array = substr($user_array, 0, -1) ?: '';
5013
                    } else {
5014
                        if (!empty($studentChoice)) {
5015
                            $correctAnswerId[] = $answerAutoId;
5016
                            $newquestionList[] = $questionId;
5017
                        }
5018
5019
                        if (1 === $answerId && isset($choice[$answerId])) {
5020
                            $studentChoice = $choice[$answerId];
5021
                            $questionScore += $answerWeighting;
5022
                        }
5023
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5024
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
5025
                        }
5026
                    }
5027
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
5028
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
5029
5030
                    break;
5031
                case FILL_IN_BLANKS_COMBINATION:
5032
                        $answerFromDatabase = '';
5033
                        if ($from_database) {
5034
                            $sql = "SELECT answer
5035
                            FROM $TBL_TRACK_ATTEMPT
5036
                            WHERE exe_id = $exeId AND question_id = $questionId";
5037
                            $result = Database::query($sql);
5038
                            $answerFromDatabase = Database::result($result, 0, 'answer');
5039
                        }
5040
5041
                        // Teacher definition and weights
5042
                        $info = FillBlanks::getAnswerInfo($answer);
5043
                        $listCorrectAnswers = $info; // keep words, weighting, switchable
5044
                        $switchableAnswerSet = $info['switchable'];
5045
                        $answerWeighting = $info['weighting'];
5046
5047
                        // Student choices
5048
                        if ($from_database && $answerFromDatabase !== '') {
5049
                            $studentInfo = FillBlanks::getAnswerInfo($answerFromDatabase, true);
5050
                            $choice = $studentInfo['student_answer'] ?? [];
5051
                        }
5052
5053
                        // Evaluate each blank and fill student_answer/student_score only
5054
                        if (!$switchableAnswerSet) {
5055
                            // Fixed positions
5056
                            $count = count($info['words']);
5057
                            for ($i = 0; $i < $count; $i++) {
5058
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
5059
                                if (!$from_database) {
5060
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
5061
                                }
5062
                                $correctAnswer = $info['words'][$i];
5063
5064
                                // Resolve menu placeholders when present (for display)
5065
                                $studentAnswerToShow = $studentAnswer;
5066
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
5067
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type && $studentAnswer !== '') {
5068
                                    $menu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
5069
                                    foreach ($menu as $opt) {
5070
                                        if (sha1($opt) === $studentAnswer) {
5071
                                            $studentAnswerToShow = $opt;
5072
                                            break;
5073
                                        }
5074
                                    }
5075
                                }
5076
5077
                                $ok = FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database);
5078
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
5079
                                $listCorrectAnswers['student_score'][$i]  = $ok ? 1 : 0;
5080
                            }
5081
                        } else {
5082
                            // Switchable answers: any student entry can match any teacher word once
5083
                            $studentTemp = (array) ($choice ?? []);
5084
                            $teacherTemp = $info['words'];
5085
5086
                            $count = max(count($studentTemp), count($teacherTemp));
5087
                            for ($i = 0; $i < $count; $i++) {
5088
                                $studentAnswer = isset($studentTemp[$i]) ? trim($studentTemp[$i]) : '';
5089
                                $studentAnswerToShow = $studentAnswer;
5090
5091
                                if ($studentAnswer !== '') {
5092
                                    if (!$from_database) {
5093
                                        $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
5094
                                    }
5095
                                    $found = false;
5096
                                    foreach ($teacherTemp as $j => $correctAnswer) {
5097
                                        if ($found) {
5098
                                            continue;
5099
                                        }
5100
                                        if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
5101
                                            // consume this teacher slot
5102
                                            $teacherTemp[$j] = '';
5103
                                            $found = true;
5104
                                        }
5105
                                    }
5106
5107
                                    // Resolve menu placeholder for display
5108
                                    if ($studentAnswerToShow !== '') {
5109
                                        $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer ?? '');
5110
                                        if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
5111
                                            $menu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
5112
                                            foreach ($menu as $opt) {
5113
                                                if (sha1($opt) === $studentAnswer) {
5114
                                                    $studentAnswerToShow = $opt;
5115
                                                    break;
5116
                                                }
5117
                                            }
5118
                                        }
5119
                                    }
5120
5121
                                    $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
5122
                                    $listCorrectAnswers['student_score'][$i]  = $found ? 1 : 0;
5123
                                } else {
5124
                                    $listCorrectAnswers['student_answer'][$i] = '';
5125
                                    $listCorrectAnswers['student_score'][$i]  = 0;
5126
                                }
5127
                            }
5128
                        }
5129
5130
                        // Build the annotated answer string for display
5131
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
5132
                    break;
5133
                case HOT_SPOT_COMBINATION:
5134
                        if ($from_database) {
5135
                            $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5136
                            // Try by auto id
5137
                            $sql = "SELECT hotspot_correct
5138
                                FROM $TBL_TRACK_HOTSPOT
5139
                                WHERE hotspot_exe_id = $exeId
5140
                                  AND hotspot_question_id = $questionId
5141
                                  AND hotspot_answer_id = $answerAutoId
5142
                                ORDER BY hotspot_id ASC";
5143
                            $result = Database::query($sql);
5144
                            if (Database::num_rows($result)) {
5145
                                $studentChoice = (int) Database::result($result, 0, 'hotspot_correct');
5146
                                $choice[$answerAutoId] = $studentChoice ? 1 : 0;
5147
                            } else {
5148
                                // Fallback to legacy ids
5149
                                $studentChoice = 0;
5150
                                if (!empty($answerIid)) {
5151
                                    $sql = "SELECT hotspot_correct
5152
                                        FROM $TBL_TRACK_HOTSPOT
5153
                                        WHERE hotspot_exe_id = $exeId
5154
                                          AND hotspot_question_id = $questionId
5155
                                          AND hotspot_answer_id = $answerIid
5156
                                        ORDER BY hotspot_id ASC";
5157
                                    $result = Database::query($sql);
5158
                                    if (Database::num_rows($result)) {
5159
                                        $studentChoice = (int) Database::result($result, 0, 'hotspot_correct');
5160
                                    }
5161
                                }
5162
                                $choice[$answerAutoId] = $studentChoice ? 1 : 0;
5163
                                $choice[$answerIid]    = $choice[$answerAutoId];
5164
                            }
5165
                        } else {
5166
                            // Validate coordinate inside the expected shape and mark $choice
5167
                            if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
5168
                                $choice[$answerAutoId] = 0;
5169
                                $choice[$answerIid]    = 0;
5170
                                $studentChoice = 0;
5171
                            } else {
5172
                                $studentChoicePoint = $choice[$answerAutoId] ?? $choice[$answerIid] ?? '';
5173
                                $isValid = false;
5174
5175
                                if (!empty($studentChoicePoint)) {
5176
                                    $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
5177
                                    $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
5178
                                    $choicePoint = Geometry::decodePoint($studentChoicePoint);
5179
5180
                                    switch ($hotspotType) {
5181
                                        case 'square':
5182
                                            $props = Geometry::decodeSquare($hotspotCoordinates);
5183
                                            $isValid = Geometry::pointIsInSquare($props, $choicePoint);
5184
                                            break;
5185
                                        case 'circle':
5186
                                            $props = Geometry::decodeEllipse($hotspotCoordinates);
5187
                                            $isValid = Geometry::pointIsInEllipse($props, $choicePoint);
5188
                                            break;
5189
                                        case 'poly':
5190
                                            $props = Geometry::decodePolygon($hotspotCoordinates);
5191
                                            $isValid = Geometry::pointIsInPolygon($props, $choicePoint);
5192
                                            break;
5193
                                    }
5194
                                }
5195
5196
                                $choice[$answerAutoId] = $isValid ? 1 : 0;
5197
                                $choice[$answerIid]    = $choice[$answerAutoId];
5198
                                $studentChoice         = $choice[$answerAutoId];
5199
                            }
5200
                        }
5201
                    break;
5202
                case ANNOTATION:
5203
                    if ($from_database) {
5204
                        $sql = "SELECT answer, marks
5205
                                FROM $TBL_TRACK_ATTEMPT
5206
                                WHERE
5207
                                  exe_id = $exeId AND
5208
                                  question_id = $questionId ";
5209
                        $resq = Database::query($sql);
5210
                        $data = Database::fetch_array($resq);
5211
5212
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
5213
                        $arrques = $questionName;
5214
5215
                        break;
5216
                    }
5217
                    $studentChoice = $choice;
5218
                    if ($studentChoice) {
5219
                        $questionScore = 0;
5220
                    }
5221
5222
                    break;
5223
            }
5224
5225
            if ($show_result) {
5226
                if ('exercise_result' === $from) {
5227
                    // Display answers (if not matching type, or if the answer is correct)
5228
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION]) ||
5229
                        $answerCorrect
5230
                    ) {
5231
                        if (in_array(
5232
                            $answerType,
5233
                            [
5234
                                UNIQUE_ANSWER,
5235
                                UNIQUE_ANSWER_IMAGE,
5236
                                UNIQUE_ANSWER_NO_OPTION,
5237
                                MULTIPLE_ANSWER,
5238
                                MULTIPLE_ANSWER_COMBINATION,
5239
                                GLOBAL_MULTIPLE_ANSWER,
5240
                                READING_COMPREHENSION,
5241
                            ]
5242
                        )) {
5243
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
5244
                                $this,
5245
                                $feedback_type,
5246
                                $answerType,
5247
                                $studentChoice,
5248
                                $answer,
5249
                                $answerComment,
5250
                                $answerCorrect,
5251
                                0,
5252
                                0,
5253
                                0,
5254
                                $results_disabled,
5255
                                $showTotalScoreAndUserChoicesInLastAttempt,
5256
                                $this->export
5257
                            );
5258
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
5259
                            ExerciseShowFunctions::display_multiple_answer_true_false(
5260
                                $this,
5261
                                $feedback_type,
5262
                                $answerType,
5263
                                $studentChoice,
5264
                                $answer,
5265
                                $answerComment,
5266
                                $answerCorrect,
5267
                                0,
5268
                                $questionId,
5269
                                0,
5270
                                $results_disabled,
5271
                                $showTotalScoreAndUserChoicesInLastAttempt
5272
                            );
5273
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
5274
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5275
                                $this,
5276
                                $feedback_type,
5277
                                $studentChoice,
5278
                                $studentChoiceDegree,
5279
                                $answer,
5280
                                $answerComment,
5281
                                $answerCorrect,
5282
                                $questionId,
5283
                                $results_disabled
5284
                            );
5285
                        } elseif (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) {
5286
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5287
                                $this,
5288
                                $feedback_type,
5289
                                $answerType,
5290
                                $studentChoice,
5291
                                $answer,
5292
                                $answerComment,
5293
                                $answerCorrect,
5294
                                0,
5295
                                0,
5296
                                0,
5297
                                $results_disabled,
5298
                                $showTotalScoreAndUserChoicesInLastAttempt
5299
                            );
5300
                        } elseif (in_array($answerType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
5301
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5302
                                $this,
5303
                                $feedback_type,
5304
                                $answer,
5305
                                0,
5306
                                0,
5307
                                $results_disabled,
5308
                                $showTotalScoreAndUserChoicesInLastAttempt,
5309
                                ''
5310
                            );
5311
                        } elseif (CALCULATED_ANSWER == $answerType) {
5312
                            ExerciseShowFunctions::display_calculated_answer(
5313
                                $this,
5314
                                $feedback_type,
5315
                                $answer,
5316
                                0,
5317
                                0,
5318
                                $results_disabled,
5319
                                $showTotalScoreAndUserChoicesInLastAttempt,
5320
                                $expectedAnswer,
5321
                                $calculatedChoice,
5322
                                $calculatedStatus
5323
                            );
5324
                        } elseif (FREE_ANSWER == $answerType) {
5325
                            ExerciseShowFunctions::display_free_answer(
5326
                                $feedback_type,
5327
                                $choice,
5328
                                $exeId,
5329
                                $questionId,
5330
                                $questionScore,
5331
                                $results_disabled
5332
                            );
5333
                        } elseif ($answerType == UPLOAD_ANSWER) {
5334
                            ExerciseShowFunctions::displayUploadAnswer(
5335
                                $feedback_type,
5336
                                $choice,
5337
                                $exeId,
5338
                                $questionId,
5339
                                $questionScore,
5340
                                $results_disabled
5341
                            );
5342
                        } elseif (ORAL_EXPRESSION == $answerType) {
5343
                            // to store the details of open questions in an array to be used in mail
5344
                            /** @var OralExpression $objQuestionTmp */
5345
                            ExerciseShowFunctions::display_oral_expression_answer(
5346
                                $feedback_type,
5347
                                $choice,
5348
                                $exeId,
5349
                                $questionId,
5350
                                $results_disabled,
5351
                                $questionScore,
5352
                                true
5353
                            );
5354
                        } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION], true)) {
5355
                            $correctAnswerId = 0;
5356
                            /** @var TrackEHotspot $hotspot */
5357
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5358
                                if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
5359
                                    break;
5360
                                }
5361
                            }
5362
5363
                            // force to show whether the choice is correct or not
5364
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5365
                            ExerciseShowFunctions::display_hotspot_answer(
5366
                                $this,
5367
                                $feedback_type,
5368
                                $answerId,
5369
                                $answer,
5370
                                $studentChoice,
5371
                                $answerComment,
5372
                                $results_disabled,
5373
                                $answerId,
5374
                                $showTotalScoreAndUserChoicesInLastAttempt
5375
                            );
5376
                        } elseif (HOT_SPOT_ORDER == $answerType) {
5377
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5378
                                $feedback_type,
5379
                                $answerId,
5380
                                $answer,
5381
                                $studentChoice,
5382
                                $answerComment
5383
                            );*/
5384
                        } elseif (HOT_SPOT_DELINEATION == $answerType && isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5385
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5386
5387
                            // Round-up the coordinates
5388
                            $coords = explode('/', $user_answer);
5389
                            $coords = array_filter($coords);
5390
                            $user_array = '';
5391
                            foreach ($coords as $coord) {
5392
                                if (!empty($coord)) {
5393
                                    $parts = explode(';', $coord);
5394
                                    if (!empty($parts)) {
5395
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5396
                                    }
5397
                                }
5398
                            }
5399
                            $user_array = substr($user_array, 0, -1) ?: '';
5400
                            if ($next) {
5401
                                $user_answer = $user_array;
5402
                                // We compare only the delineation not the other points
5403
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5404
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5405
5406
                                // Calculating the area
5407
                                $poly_user = convert_coordinates($user_answer, '/');
5408
                                $poly_answer = convert_coordinates($answer_question, '|');
5409
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5410
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5411
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5412
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5413
5414
                                $overlap = $poly_results['both'];
5415
                                $poly_answer_area = $poly_results['s1'];
5416
                                $poly_user_area = $poly_results['s2'];
5417
                                $missing = $poly_results['s1Only'];
5418
                                $excess = $poly_results['s2Only'];
5419
5420
                                // //this is an area in pixels
5421
                                if ($debug > 0) {
5422
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5423
                                }
5424
5425
                                if ($overlap < 1) {
5426
                                    // Shortcut to avoid complicated calculations
5427
                                    $final_overlap = 0;
5428
                                    $final_missing = 100;
5429
                                    $final_excess = 100;
5430
                                } else {
5431
                                    // the final overlap is the percentage of the initial polygon
5432
                                    // that is overlapped by the user's polygon
5433
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5434
                                    if ($debug > 1) {
5435
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5436
                                    }
5437
                                    // the final missing area is the percentage of the initial polygon
5438
                                    // that is not overlapped by the user's polygon
5439
                                    $final_missing = 100 - $final_overlap;
5440
                                    if ($debug > 1) {
5441
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5442
                                    }
5443
                                    // the final excess area is the percentage of the initial polygon's size
5444
                                    // that is covered by the user's polygon outside of the initial polygon
5445
                                    $final_excess = round(
5446
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5447
                                    );
5448
                                    if ($debug > 1) {
5449
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5450
                                    }
5451
                                }
5452
5453
                                // Checking the destination parameters parsing the "@@"
5454
                                $destination_items = explode('@@', $answerDestination);
5455
                                $threadhold_total = $destination_items[0];
5456
                                $threadhold_items = explode(';', $threadhold_total);
5457
                                $threadhold1 = $threadhold_items[0]; // overlap
5458
                                $threadhold2 = $threadhold_items[1]; // excess
5459
                                $threadhold3 = $threadhold_items[2]; // missing
5460
5461
                                // if is delineation
5462
                                if (1 === $answerId) {
5463
                                    //setting colors
5464
                                    if ($final_overlap >= $threadhold1) {
5465
                                        $overlap_color = true;
5466
                                    }
5467
                                    if ($final_excess <= $threadhold2) {
5468
                                        $excess_color = true;
5469
                                    }
5470
                                    if ($final_missing <= $threadhold3) {
5471
                                        $missing_color = true;
5472
                                    }
5473
5474
                                    // if pass
5475
                                    if ($final_overlap >= $threadhold1 &&
5476
                                        $final_missing <= $threadhold3 &&
5477
                                        $final_excess <= $threadhold2
5478
                                    ) {
5479
                                        $next = 1; //go to the oars
5480
                                        $result_comment = get_lang('Acceptable');
5481
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5482
                                    } else {
5483
                                        $next = 0;
5484
                                        $result_comment = get_lang('Unacceptable');
5485
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5486
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5487
                                        // checking the destination parameters parsing the "@@"
5488
                                        $destination_items = explode('@@', $answerDestination);
5489
                                    }
5490
                                } elseif ($answerId > 1) {
5491
                                    if ('noerror' == $objAnswerTmp->selectHotspotType($answerId)) {
5492
                                        if ($debug > 0) {
5493
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5494
                                        }
5495
                                        //type no error shouldn't be treated
5496
                                        $next = 1;
5497
5498
                                        continue;
5499
                                    }
5500
                                    if ($debug > 0) {
5501
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5502
                                    }
5503
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5504
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5505
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5506
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5507
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5508
5509
                                    if (false == $overlap) {
5510
                                        //all good, no overlap
5511
                                        $next = 1;
5512
5513
                                        continue;
5514
                                    } else {
5515
                                        if ($debug > 0) {
5516
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5517
                                        }
5518
                                        $organs_at_risk_hit++;
5519
                                        //show the feedback
5520
                                        $next = 0;
5521
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5522
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5523
5524
                                        $destination_items = explode('@@', $answerDestination);
5525
                                        $try_hotspot = $destination_items[1];
5526
                                        $lp_hotspot = $destination_items[2];
5527
                                        $select_question_hotspot = $destination_items[3];
5528
                                        $url_hotspot = $destination_items[4];
5529
                                    }
5530
                                }
5531
                            } else {
5532
                                // the first delineation feedback
5533
                                if ($debug > 0) {
5534
                                    error_log(__LINE__.' first', 0);
5535
                                }
5536
                            }
5537
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
5538
                            echo '<tr>';
5539
                            echo Display::tag('td', $answerMatching[$answerId]);
5540
                            echo Display::tag(
5541
                                'td',
5542
                                "$user_answer / ".Display::tag(
5543
                                    'strong',
5544
                                    $answerMatching[$answerCorrect],
5545
                                    ['style' => 'color: #008000; font-weight: bold;']
5546
                                )
5547
                            );
5548
                            echo '</tr>';
5549
                        } elseif (ANNOTATION == $answerType) {
5550
                            ExerciseShowFunctions::displayAnnotationAnswer(
5551
                                $feedback_type,
5552
                                $exeId,
5553
                                $questionId,
5554
                                $questionScore,
5555
                                $results_disabled
5556
                            );
5557
                        }
5558
                    }
5559
                } else {
5560
                    if ($debug) {
5561
                        error_log('Showing questions $from '.$from);
5562
                    }
5563
5564
                    switch ($answerType) {
5565
                        case UNIQUE_ANSWER:
5566
                        case UNIQUE_ANSWER_IMAGE:
5567
                        case UNIQUE_ANSWER_NO_OPTION:
5568
                        case MULTIPLE_ANSWER:
5569
                        case GLOBAL_MULTIPLE_ANSWER:
5570
                        case MULTIPLE_ANSWER_COMBINATION:
5571
                        case READING_COMPREHENSION:
5572
                            if (1 == $answerId) {
5573
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5574
                                    $this,
5575
                                    $feedback_type,
5576
                                    $answerType,
5577
                                    $studentChoice,
5578
                                    $answer,
5579
                                    $answerComment,
5580
                                    $answerCorrect,
5581
                                    $exeId,
5582
                                    $questionId,
5583
                                    $answerId,
5584
                                    $results_disabled,
5585
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5586
                                    $this->export
5587
                                );
5588
                            } else {
5589
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5590
                                    $this,
5591
                                    $feedback_type,
5592
                                    $answerType,
5593
                                    $studentChoice,
5594
                                    $answer,
5595
                                    $answerComment,
5596
                                    $answerCorrect,
5597
                                    $exeId,
5598
                                    $questionId,
5599
                                    '',
5600
                                    $results_disabled,
5601
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5602
                                    $this->export
5603
                                );
5604
                            }
5605
5606
                            break;
5607
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5608
                            if (1 == $answerId) {
5609
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5610
                                    $this,
5611
                                    $feedback_type,
5612
                                    $answerType,
5613
                                    $studentChoice,
5614
                                    $answer,
5615
                                    $answerComment,
5616
                                    $answerCorrect,
5617
                                    $exeId,
5618
                                    $questionId,
5619
                                    $answerId,
5620
                                    $results_disabled,
5621
                                    $showTotalScoreAndUserChoicesInLastAttempt
5622
                                );
5623
                            } else {
5624
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5625
                                    $this,
5626
                                    $feedback_type,
5627
                                    $answerType,
5628
                                    $studentChoice,
5629
                                    $answer,
5630
                                    $answerComment,
5631
                                    $answerCorrect,
5632
                                    $exeId,
5633
                                    $questionId,
5634
                                    '',
5635
                                    $results_disabled,
5636
                                    $showTotalScoreAndUserChoicesInLastAttempt
5637
                                );
5638
                            }
5639
5640
                            break;
5641
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5642
                            if (1 == $answerId) {
5643
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5644
                                    $this,
5645
                                    $feedback_type,
5646
                                    $answerType,
5647
                                    $studentChoice,
5648
                                    $answer,
5649
                                    $answerComment,
5650
                                    $answerCorrect,
5651
                                    $exeId,
5652
                                    $questionId,
5653
                                    $answerId,
5654
                                    $results_disabled,
5655
                                    $showTotalScoreAndUserChoicesInLastAttempt
5656
                                );
5657
                            } else {
5658
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5659
                                    $this,
5660
                                    $feedback_type,
5661
                                    $answerType,
5662
                                    $studentChoice,
5663
                                    $answer,
5664
                                    $answerComment,
5665
                                    $answerCorrect,
5666
                                    $exeId,
5667
                                    $questionId,
5668
                                    '',
5669
                                    $results_disabled,
5670
                                    $showTotalScoreAndUserChoicesInLastAttempt
5671
                                );
5672
                            }
5673
5674
                            break;
5675
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5676
                            if (1 == $answerId) {
5677
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5678
                                    $this,
5679
                                    $feedback_type,
5680
                                    $studentChoice,
5681
                                    $studentChoiceDegree,
5682
                                    $answer,
5683
                                    $answerComment,
5684
                                    $answerCorrect,
5685
                                    $questionId,
5686
                                    $results_disabled
5687
                                );
5688
                            } else {
5689
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5690
                                    $this,
5691
                                    $feedback_type,
5692
                                    $studentChoice,
5693
                                    $studentChoiceDegree,
5694
                                    $answer,
5695
                                    $answerComment,
5696
                                    $answerCorrect,
5697
                                    $questionId,
5698
                                    $results_disabled
5699
                                );
5700
                            }
5701
5702
                            break;
5703
                        case FILL_IN_BLANKS:
5704
                        case FILL_IN_BLANKS_COMBINATION:
5705
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5706
                                $this,
5707
                                $feedback_type,
5708
                                $answer,
5709
                                $exeId,
5710
                                $questionId,
5711
                                $results_disabled,
5712
                                $showTotalScoreAndUserChoicesInLastAttempt,
5713
                                $str
5714
                            );
5715
                            break;
5716
                        case CALCULATED_ANSWER:
5717
                            ExerciseShowFunctions::display_calculated_answer(
5718
                                $this,
5719
                                $feedback_type,
5720
                                $answer,
5721
                                $exeId,
5722
                                $questionId,
5723
                                $results_disabled,
5724
                                '',
5725
                                $showTotalScoreAndUserChoicesInLastAttempt
5726
                            );
5727
5728
                            break;
5729
                        case FREE_ANSWER:
5730
                            echo ExerciseShowFunctions::display_free_answer(
5731
                                $feedback_type,
5732
                                $choice,
5733
                                $exeId,
5734
                                $questionId,
5735
                                $questionScore,
5736
                                $results_disabled
5737
                            );
5738
5739
                            break;
5740
                        case UPLOAD_ANSWER:
5741
                            echo ExerciseShowFunctions::displayUploadAnswer(
5742
                                $feedback_type,
5743
                                $choice,
5744
                                $exeId,
5745
                                $questionId,
5746
                                $questionScore,
5747
                                $results_disabled
5748
                            );
5749
                            break;
5750
                        case ORAL_EXPRESSION:
5751
                            /** @var OralExpression $objQuestionTmp */
5752
                            echo '<tr>
5753
                                <td valign="top">'.
5754
                                ExerciseShowFunctions::display_oral_expression_answer(
5755
                                    $feedback_type,
5756
                                    $choice,
5757
                                    $exeId,
5758
                                    $questionId,
5759
                                    $results_disabled,
5760
                                    $questionScore
5761
                                ).'</td>
5762
                                </tr>
5763
                                </table>';
5764
                            break;
5765
                        case HOT_SPOT:
5766
                        case HOT_SPOT_COMBINATION:
5767
                            $correctAnswerId = 0;
5768
5769
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5770
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5771
                                    break;
5772
                                }
5773
                            }
5774
                            ExerciseShowFunctions::display_hotspot_answer(
5775
                                $this,
5776
                                $feedback_type,
5777
                                $answerId,
5778
                                $answer,
5779
                                $studentChoice,
5780
                                $answerComment,
5781
                                $results_disabled,
5782
                                $answerId,
5783
                                $showTotalScoreAndUserChoicesInLastAttempt
5784
                            );
5785
5786
                            break;
5787
                        case HOT_SPOT_DELINEATION:
5788
                            $user_answer = $user_array;
5789
                            if ($next) {
5790
                                $user_answer = $user_array;
5791
                                // we compare only the delineation not the other points
5792
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5793
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5794
5795
                                // calculating the area
5796
                                $poly_user = convert_coordinates($user_answer, '/');
5797
                                $poly_answer = convert_coordinates($answer_question, '|');
5798
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5799
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5800
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5801
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5802
5803
                                $overlap = $poly_results['both'];
5804
                                $poly_answer_area = $poly_results['s1'];
5805
                                $poly_user_area = $poly_results['s2'];
5806
                                $missing = $poly_results['s1Only'];
5807
                                $excess = $poly_results['s2Only'];
5808
                                if ($debug > 0) {
5809
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5810
                                }
5811
                                if ($overlap < 1) {
5812
                                    //shortcut to avoid complicated calculations
5813
                                    $final_overlap = 0;
5814
                                    $final_missing = 100;
5815
                                    $final_excess = 100;
5816
                                } else {
5817
                                    // the final overlap is the percentage of the initial polygon
5818
                                    // that is overlapped by the user's polygon
5819
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5820
5821
                                    // the final missing area is the percentage of the initial polygon that
5822
                                    // is not overlapped by the user's polygon
5823
                                    $final_missing = 100 - $final_overlap;
5824
                                    // the final excess area is the percentage of the initial polygon's size that is
5825
                                    // covered by the user's polygon outside of the initial polygon
5826
                                    $final_excess = round(
5827
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5828
                                    );
5829
5830
                                    if ($debug > 1) {
5831
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5832
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5833
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5834
                                    }
5835
                                }
5836
5837
                                // Checking the destination parameters parsing the "@@"
5838
                                $destination_items = explode('@@', $answerDestination);
5839
                                $threadhold_total = $destination_items[0];
5840
                                $threadhold_items = explode(';', $threadhold_total);
5841
                                $threadhold1 = $threadhold_items[0]; // overlap
5842
                                $threadhold2 = $threadhold_items[1]; // excess
5843
                                $threadhold3 = $threadhold_items[2]; //missing
5844
                                // if is delineation
5845
                                if (1 === $answerId) {
5846
                                    //setting colors
5847
                                    if ($final_overlap >= $threadhold1) {
5848
                                        $overlap_color = true;
5849
                                    }
5850
                                    if ($final_excess <= $threadhold2) {
5851
                                        $excess_color = true;
5852
                                    }
5853
                                    if ($final_missing <= $threadhold3) {
5854
                                        $missing_color = true;
5855
                                    }
5856
5857
                                    // if pass
5858
                                    if ($final_overlap >= $threadhold1 &&
5859
                                        $final_missing <= $threadhold3 &&
5860
                                        $final_excess <= $threadhold2
5861
                                    ) {
5862
                                        $next = 1; //go to the oars
5863
                                        $result_comment = get_lang('Acceptable');
5864
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5865
                                    } else {
5866
                                        $next = 0;
5867
                                        $result_comment = get_lang('Unacceptable');
5868
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5869
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5870
                                        //checking the destination parameters parsing the "@@"
5871
                                        $destination_items = explode('@@', $answerDestination);
5872
                                    }
5873
                                } elseif ($answerId > 1) {
5874
                                    if ('noerror' === $objAnswerTmp->selectHotspotType($answerId)) {
5875
                                        if ($debug > 0) {
5876
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5877
                                        }
5878
                                        //type no error shouldn't be treated
5879
                                        $next = 1;
5880
5881
                                        break;
5882
                                    }
5883
                                    if ($debug > 0) {
5884
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5885
                                    }
5886
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5887
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5888
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5889
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5890
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5891
5892
                                    if (false == $overlap) {
5893
                                        //all good, no overlap
5894
                                        $next = 1;
5895
5896
                                        break;
5897
                                    } else {
5898
                                        if ($debug > 0) {
5899
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5900
                                        }
5901
                                        $organs_at_risk_hit++;
5902
                                        //show the feedback
5903
                                        $next = 0;
5904
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5905
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5906
5907
                                        $destination_items = explode('@@', $answerDestination);
5908
                                        $try_hotspot = $destination_items[1];
5909
                                        $lp_hotspot = $destination_items[2];
5910
                                        $select_question_hotspot = $destination_items[3];
5911
                                        $url_hotspot = $destination_items[4];
5912
                                    }
5913
                                }
5914
                            }
5915
5916
                            break;
5917
                        case HOT_SPOT_ORDER:
5918
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5919
                                $feedback_type,
5920
                                $answerId,
5921
                                $answer,
5922
                                $studentChoice,
5923
                                $answerComment
5924
                            );*/
5925
5926
                            break;
5927
                        case DRAGGABLE:
5928
                        case MATCHING_DRAGGABLE:
5929
                        case MATCHING:
5930
                        case MATCHING_COMBINATION:
5931
                        case MATCHING_DRAGGABLE_COMBINATION:
5932
                            echo '<tr>';
5933
                            echo Display::tag('td', $answerMatching[$answerId]);
5934
                            echo Display::tag(
5935
                                'td',
5936
                                "$user_answer / ".Display::tag(
5937
                                    'strong',
5938
                                    $answerMatching[$answerCorrect],
5939
                                    ['style' => 'color: #008000; font-weight: bold;']
5940
                                )
5941
                            );
5942
                            echo '</tr>';
5943
5944
                            break;
5945
                        case ANNOTATION:
5946
                            ExerciseShowFunctions::displayAnnotationAnswer(
5947
                                $feedback_type,
5948
                                $exeId,
5949
                                $questionId,
5950
                                $questionScore,
5951
                                $results_disabled
5952
                            );
5953
5954
                            break;
5955
                    }
5956
                }
5957
            }
5958
        } // end for that loops over all answers of the current question
5959
5960
        if ($debug) {
5961
            error_log('-- End answer loop --');
5962
        }
5963
5964
        $final_answer = true;
5965
5966
        foreach ($real_answers as $my_answer) {
5967
            if (!$my_answer) {
5968
                $final_answer = false;
5969
            }
5970
        }
5971
5972
        if (FILL_IN_BLANKS_COMBINATION === $answerType) {
5973
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5974
                $answerType,
5975
                $listCorrectAnswers,
5976
                $exeId,
5977
                $questionId,
5978
                $questionWeighting
5979
            );
5980
        }
5981
5982
        if (HOT_SPOT_COMBINATION === $answerType) {
5983
            $listCoords = $exerciseResultCoordinates[$questionId] ?? [];
5984
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5985
                $answerType,
5986
                $listCoords,
5987
                $exeId,
5988
                $questionId,
5989
                $questionWeighting,
5990
                (array) ($choice ?? []),
5991
                $nbrAnswers
5992
            );
5993
        }
5994
5995
        if (in_array($answerType, [MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION], true)) {
5996
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5997
                $answerType,
5998
                $matchingCorrectAnswers[$questionId] ?? [],
5999
                $exeId,
6000
                $questionId,
6001
                $questionWeighting
6002
            );
6003
        }
6004
6005
        //we add the total score after dealing with the answers
6006
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
6007
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
6008
        ) {
6009
            if ($final_answer) {
6010
                //getting only the first score where we save the weight of all the question
6011
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
6012
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
6013
                    $answerWeighting = $firstAnswer['ponderation'];
6014
                }
6015
                $questionScore += $answerWeighting;
6016
            }
6017
        }
6018
6019
        $extra_data = [
6020
            'final_overlap' => $final_overlap,
6021
            'final_missing' => $final_missing,
6022
            'final_excess' => $final_excess,
6023
            'overlap_color' => $overlap_color,
6024
            'missing_color' => $missing_color,
6025
            'excess_color' => $excess_color,
6026
            'threadhold1' => $threadhold1,
6027
            'threadhold2' => $threadhold2,
6028
            'threadhold3' => $threadhold3,
6029
        ];
6030
6031
        if ('exercise_result' === $from) {
6032
            // if answer is hotspot. To the difference of exercise_show.php,
6033
            //  we use the results from the session (from_db=0)
6034
            // TODO Change this, because it is wrong to show the user
6035
            //  some results that haven't been stored in the database yet
6036
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_DELINEATION, HOT_SPOT_COMBINATION])) {
6037
                if ($debug) {
6038
                    error_log('$from AND this is a hotspot kind of question ');
6039
                }
6040
                if (HOT_SPOT_DELINEATION === $answerType) {
6041
                    if ($showHotSpotDelineationTable) {
6042
                        if (!is_numeric($final_overlap)) {
6043
                            $final_overlap = 0;
6044
                        }
6045
                        if (!is_numeric($final_missing)) {
6046
                            $final_missing = 0;
6047
                        }
6048
                        if (!is_numeric($final_excess)) {
6049
                            $final_excess = 0;
6050
                        }
6051
6052
                        if ($final_overlap > 100) {
6053
                            $final_overlap = 100;
6054
                        }
6055
6056
                        $overlap = 0;
6057
                        if ($final_overlap > 0) {
6058
                            $overlap = (int) $final_overlap;
6059
                        }
6060
6061
                        $excess = 0;
6062
                        if ($final_excess > 0) {
6063
                            $excess = (int) $final_excess;
6064
                        }
6065
6066
                        $missing = 0;
6067
                        if ($final_missing > 0) {
6068
                            $missing = (int) $final_missing;
6069
                        }
6070
6071
                        $table_resume = '<table class="table table-hover table-striped data_table">
6072
                                <tr class="row_odd" >
6073
                                    <td></td>
6074
                                    <td ><b>'.get_lang('Requirements').'</b></td>
6075
                                    <td><b>'.get_lang('Your answer').'</b></td>
6076
                                </tr>
6077
                                <tr class="row_even">
6078
                                    <td><b>'.get_lang('Overlapping area').'</b></td>
6079
                                    <td>'.get_lang('Minimum').' '.$threadhold1.'</td>
6080
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-error').'">'
6081
                                    .$overlap.'</td>
6082
                                </tr>
6083
                                <tr>
6084
                                    <td><b>'.get_lang('Excessive area').'</b></td>
6085
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
6086
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-error').'">'
6087
                                    .$excess.'</td>
6088
                                </tr>
6089
                                <tr class="row_even">
6090
                                    <td><b>'.get_lang('Missing area').'</b></td>
6091
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
6092
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-error').'">'
6093
                                    .$missing.'</td>
6094
                                </tr>
6095
                            </table>';
6096
                        if (0 == $next) {
6097
                        } else {
6098
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
6099
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
6100
                        }
6101
6102
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
6103
                                    <p style="text-align:center">';
6104
                        $message .= '<p>'.get_lang('Your delineation :').'</p>';
6105
                        $message .= $table_resume;
6106
                        $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
6107
                        if ($organs_at_risk_hit > 0) {
6108
                            $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
6109
                        }
6110
                        $message .= '<p>'.$comment.'</p>';
6111
                        echo $message;
6112
6113
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][0] = $message;
6114
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
6115
                            $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
6116
                        }
6117
                    } else {
6118
                        echo $hotspot_delineation_result[0] ?? '';
6119
                    }
6120
6121
                    // Save the score attempts
6122
                    if (1) {
6123
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
6124
                        $final_answer = $hotspot_delineation_result[1] ?? '';
6125
                        if (0 == $final_answer) {
6126
                            $questionScore = 0;
6127
                        }
6128
                        // we always insert the answer_id 1 = delineation
6129
                        Event::saveQuestionAttempt($this, $questionScore, 1, $quesId, $exeId, 0);
6130
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
6131
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? 1 === (int) $hotspot_delineation_result[1] ? 1 : 0 : 0;
6132
                        Event::saveExerciseAttemptHotspot(
6133
                            $this,
6134
                            $exeId,
6135
                            $quesId,
6136
                            1,
6137
                            $hotspotValue,
6138
                            $exerciseResultCoordinates[$quesId] ?? '',
6139
                            false,
6140
                            0,
6141
                            $learnpath_id,
6142
                            $learnpath_item_id
6143
                        );
6144
                    } else {
6145
                        if (0 == $final_answer) {
6146
                            $questionScore = 0;
6147
                            $answer = 0;
6148
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
6149
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6150
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6151
                                    Event::saveExerciseAttemptHotspot(
6152
                                        $this,
6153
                                        $exeId,
6154
                                        $quesId,
6155
                                        $idx,
6156
                                        0,
6157
                                        $val,
6158
                                        false,
6159
                                        0,
6160
                                        $learnpath_id,
6161
                                        $learnpath_item_id
6162
                                    );
6163
                                }
6164
                            }
6165
                        } else {
6166
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
6167
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6168
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6169
                                    $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6170
                                    Event::saveExerciseAttemptHotspot(
6171
                                        $this,
6172
                                        $exeId,
6173
                                        $quesId,
6174
                                        $idx,
6175
                                        $hotspotValue,
6176
                                        $val,
6177
                                        false,
6178
                                        0,
6179
                                        $learnpath_id,
6180
                                        $learnpath_item_id
6181
                                    );
6182
                                }
6183
                            }
6184
                        }
6185
                    }
6186
                }
6187
            }
6188
6189
            $relPath = api_get_path(WEB_CODE_PATH);
6190
6191
            if ($save_results
6192
                && in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION], true)
6193
                && !empty($exerciseResultCoordinates[$questionId])
6194
            ) {
6195
                Database::delete(
6196
                    Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6197
                    [
6198
                        'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6199
                            $exeId, $questionId, api_get_course_int_id(),
6200
                        ],
6201
                    ]
6202
                );
6203
6204
                foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6205
                    $hotspotValue = !empty($choice[$idx]) ? 1 : 0;
6206
                    Event::saveExerciseAttemptHotspot(
6207
                        $this,
6208
                        $exeId,
6209
                        $quesId,
6210
                        $idx,
6211
                        $hotspotValue,
6212
                        $val,
6213
                        false,
6214
                        $this->id,
6215
                        $learnpath_id,
6216
                        $learnpath_item_id
6217
                    );
6218
                }
6219
                $hotspotsSavedEarly = true;
6220
            }
6221
6222
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_COMBINATION], true)) {
6223
                // We made an extra table for the answers
6224
                if ($show_result) {
6225
                    echo '</table></td></tr>';
6226
                    echo '
6227
                        <tr>
6228
                            <td colspan="2">
6229
                                <p><em>'.get_lang('Image zones')."</em></p>
6230
                                <div id=\"hotspot-solution-$questionId\"></div>
6231
                                <script>
6232
                                    $(function() {
6233
                                        new HotspotQuestion({
6234
                                            questionId: $questionId,
6235
                                            exerciseId: {$this->getId()},
6236
                                            exeId: $exeId,
6237
                                            selector: '#hotspot-solution-$questionId',
6238
                                            for: 'solution',
6239
                                            relPath: '$relPath'
6240
                                        });
6241
                                    });
6242
                                </script>
6243
                            </td>
6244
                        </tr>
6245
                    ";
6246
                }
6247
            } elseif (ANNOTATION == $answerType) {
6248
                if ($show_result) {
6249
                    echo '
6250
                        <p><em>'.get_lang('Annotation').'</em></p>
6251
                        <div id="annotation-canvas-'.$questionId.'"></div>
6252
                        <script>
6253
                            AnnotationQuestion({
6254
                                questionId: parseInt('.$questionId.'),
6255
                                exerciseId: parseInt('.$exeId.'),
6256
                                relPath: \''.$relPath.'\',
6257
                                courseId: parseInt('.$course_id.')
6258
                            });
6259
                        </script>
6260
                    ';
6261
                }
6262
            }
6263
6264
            if ($show_result && ANNOTATION != $answerType) {
6265
                echo '</table>';
6266
            }
6267
        }
6268
        unset($objAnswerTmp);
6269
6270
        $totalWeighting += $questionWeighting;
6271
        // Store results directly in the database
6272
        // For all in one page exercises, the results will be
6273
        // stored by exercise_results.php (using the session)
6274
        if (in_array(
6275
            $objQuestionTmp->type,
6276
            [PAGE_BREAK, MEDIA_QUESTION],
6277
            true
6278
        )) {
6279
            $save_results = false;
6280
        }
6281
        if ($save_results) {
6282
            if ($debug) {
6283
                error_log("Save question results $save_results");
6284
                error_log("Question score: $questionScore");
6285
                error_log('choice: ');
6286
                error_log(print_r($choice, 1));
6287
            }
6288
6289
            if (empty($choice)) {
6290
                $choice = 0;
6291
            }
6292
            // with certainty degree
6293
            if (empty($choiceDegreeCertainty)) {
6294
                $choiceDegreeCertainty = 0;
6295
            }
6296
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
6297
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType ||
6298
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
6299
            ) {
6300
                if (0 != $choice) {
6301
                    $reply = array_keys($choice);
6302
                    $countReply = count($reply);
6303
                    for ($i = 0; $i < $countReply; $i++) {
6304
                        $chosenAnswer = $reply[$i];
6305
                        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
6306
                            if (0 != $choiceDegreeCertainty) {
6307
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
6308
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
6309
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
6310
                                Event::saveQuestionAttempt(
6311
                                    $this,
6312
                                    $questionScore,
6313
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
6314
                                    $quesId,
6315
                                    $exeId,
6316
                                    $i,
6317
                                    $this->getId(),
6318
                                    $updateResults,
6319
                                    $questionDuration
6320
                                );
6321
                            }
6322
                        } else {
6323
                            Event::saveQuestionAttempt(
6324
                                $this,
6325
                                $questionScore,
6326
                                $chosenAnswer.':'.$choice[$chosenAnswer],
6327
                                $quesId,
6328
                                $exeId,
6329
                                $i,
6330
                                $this->getId(),
6331
                                $updateResults,
6332
                                $questionDuration
6333
                            );
6334
                        }
6335
                        if ($debug) {
6336
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
6337
                        }
6338
                    }
6339
                } else {
6340
                    Event::saveQuestionAttempt(
6341
                        $this,
6342
                        $questionScore,
6343
                        0,
6344
                        $quesId,
6345
                        $exeId,
6346
                        0,
6347
                        $this->getId(),
6348
                        false,
6349
                        $questionDuration
6350
                    );
6351
                }
6352
            } elseif (in_array($answerType, [
6353
                MULTIPLE_ANSWER,
6354
                GLOBAL_MULTIPLE_ANSWER,
6355
                MULTIPLE_ANSWER_DROPDOWN,
6356
                MULTIPLE_ANSWER_DROPDOWN_COMBINATION
6357
            ], true)) {
6358
                if (0 != $choice) {
6359
                    $reply = in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION], true)
6360
                        ? array_values((array) $choice)
6361
                        : array_keys((array) $choice);
6362
                    for ($i = 0; $i < count($reply); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
6363
                        $ans = $reply[$i];
6364
                        Event::saveQuestionAttempt(
6365
                            $this,
6366
                            $questionScore,
6367
                            $ans,
6368
                            $quesId,
6369
                            $exeId,
6370
                            $i,
6371
                            $this->id,
6372
                            false,
6373
                            $questionDuration
6374
                        );
6375
                    }
6376
                } else {
6377
                    Event::saveQuestionAttempt(
6378
                        $this,
6379
                        $questionScore,
6380
                        0,
6381
                        $quesId,
6382
                        $exeId,
6383
                        0,
6384
                        $this->id,
6385
                        false,
6386
                        $questionDuration
6387
                    );
6388
                }
6389
            } elseif (MULTIPLE_ANSWER_COMBINATION == $answerType) {
6390
                if (0 != $choice) {
6391
                    $reply = array_keys($choice);
6392
                    for ($i = 0; $i < count($reply); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
6393
                        $ans = $reply[$i];
6394
                        Event::saveQuestionAttempt(
6395
                            $this,
6396
                            $questionScore,
6397
                            $ans,
6398
                            $quesId,
6399
                            $exeId,
6400
                            $i,
6401
                            $this->id,
6402
                            false,
6403
                            $questionDuration
6404
                        );
6405
                    }
6406
                } else {
6407
                    Event::saveQuestionAttempt(
6408
                        $this,
6409
                        $questionScore,
6410
                        0,
6411
                        $quesId,
6412
                        $exeId,
6413
                        0,
6414
                        $this->id,
6415
                        false,
6416
                        $questionDuration
6417
                    );
6418
                }
6419
            } elseif (in_array($answerType, [
6420
                MATCHING,
6421
                DRAGGABLE,
6422
                MATCHING_DRAGGABLE,
6423
                MATCHING_COMBINATION,
6424
                MATCHING_DRAGGABLE_COMBINATION
6425
            ], true)) {
6426
                if (isset($matching)) {
6427
                    foreach ($matching as $j => $val) {
6428
                        Event::saveQuestionAttempt(
6429
                            $this,
6430
                            $questionScore,
6431
                            $val,
6432
                            $quesId,
6433
                            $exeId,
6434
                            $j,
6435
                            $this->id,
6436
                            false,
6437
                            $questionDuration
6438
                        );
6439
                    }
6440
                }
6441
            } elseif (FREE_ANSWER == $answerType) {
6442
                $answer = $choice;
6443
                Event::saveQuestionAttempt(
6444
                    $this,
6445
                    $questionScore,
6446
                    $answer,
6447
                    $quesId,
6448
                    $exeId,
6449
                    0,
6450
                    $this->id,
6451
                    false,
6452
                    $questionDuration
6453
                );
6454
            } elseif ($answerType == UPLOAD_ANSWER) {
6455
                $answer = '';
6456
                $questionAttemptId = Event::saveQuestionAttempt(
6457
                    $this,
6458
                    0,
6459
                    $answer,
6460
                    $quesId,
6461
                    $exeId,
6462
                    0,
6463
                    $this->id,
6464
                    false,
6465
                    $questionDuration
6466
                );
6467
6468
                if (false !== $questionAttemptId) {
6469
                    $postedAssets = isset($_REQUEST['uploadAsset'][$questionId])
6470
                        ? (array) $_REQUEST['uploadAsset'][$questionId]
6471
                        : [];
6472
                    UploadAnswer::saveAssetInQuestionAttempt($questionAttemptId, $postedAssets);
6473
                }
6474
            } elseif (ORAL_EXPRESSION == $answerType) {
6475
                $answer = $choice;
6476
                /** @var OralExpression $objQuestionTmp */
6477
                $questionAttemptId = Event::saveQuestionAttempt(
6478
                    $this,
6479
                    $questionScore,
6480
                    $answer,
6481
                    $quesId,
6482
                    $exeId,
6483
                    0,
6484
                    $this->id,
6485
                    false,
6486
                    $questionDuration
6487
                );
6488
6489
                if (false !== $questionAttemptId) {
6490
                    OralExpression::saveAssetInQuestionAttempt($questionAttemptId);
6491
                }
6492
            } elseif (
6493
            in_array(
6494
                $answerType,
6495
                [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6496
            )
6497
            ) {
6498
                $answer = $choice;
6499
                Event::saveQuestionAttempt(
6500
                    $this,
6501
                    $questionScore,
6502
                    $answer,
6503
                    $quesId,
6504
                    $exeId,
6505
                    0,
6506
                    $this->id,
6507
                    false,
6508
                    $questionDuration
6509
                );
6510
            } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, ANNOTATION], true)) {
6511
                $answer = [];
6512
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6513
                    if ($debug) {
6514
                        error_log('Checking result coordinates');
6515
                    }
6516
                    Database::delete(
6517
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6518
                        [
6519
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6520
                                $exeId,
6521
                                $questionId,
6522
                                api_get_course_int_id(),
6523
                            ],
6524
                        ]
6525
                    );
6526
6527
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6528
                        $answer[] = $val;
6529
                        $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6530
                        if ($debug) {
6531
                            error_log('Hotspot value: '.$hotspotValue);
6532
                        }
6533
                        Event::saveExerciseAttemptHotspot(
6534
                            $this,
6535
                            $exeId,
6536
                            $quesId,
6537
                            $idx,
6538
                            $hotspotValue,
6539
                            $val,
6540
                            false,
6541
                            $this->id,
6542
                            $learnpath_id,
6543
                            $learnpath_item_id
6544
                        );
6545
                    }
6546
                } else {
6547
                    if ($debug) {
6548
                        error_log('Empty: exerciseResultCoordinates');
6549
                    }
6550
                }
6551
                Event::saveQuestionAttempt(
6552
                    $this,
6553
                    $questionScore,
6554
                    implode('|', $answer),
6555
                    $quesId,
6556
                    $exeId,
6557
                    0,
6558
                    $this->id,
6559
                    false,
6560
                    $questionDuration
6561
                );
6562
            } else {
6563
                Event::saveQuestionAttempt(
6564
                    $this,
6565
                    $questionScore,
6566
                    $answer,
6567
                    $quesId,
6568
                    $exeId,
6569
                    0,
6570
                    $this->id,
6571
                    false,
6572
                    $questionDuration
6573
                );
6574
            }
6575
        }
6576
6577
        if (0 == $propagate_neg && $questionScore < 0) {
6578
            $questionScore = 0;
6579
        }
6580
6581
        if ($save_results) {
6582
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6583
            $sql = "UPDATE $statsTable SET
6584
                        score = score + ".(float) $questionScore."
6585
                    WHERE exe_id = $exeId";
6586
            Database::query($sql);
6587
        }
6588
6589
        return [
6590
            'score' => $questionScore,
6591
            'weight' => $questionWeighting,
6592
            'extra' => $extra_data,
6593
            'open_question' => $arrques,
6594
            'open_answer' => $arrans,
6595
            'answer_type' => $answerType,
6596
            'generated_oral_file' => $generatedFilesHtml,
6597
            'user_answered' => $userAnsweredQuestion,
6598
            'correct_answer_id' => $correctAnswerId,
6599
            'answer_destination' => $answerDestination,
6600
        ];
6601
    }
6602
6603
    /**
6604
     * Sends a notification when a user ends an examn.
6605
     *
6606
     * @param string $type                  'start' or 'end' of an exercise
6607
     * @param array  $question_list_answers
6608
     * @param string $origin
6609
     * @param int    $exe_id
6610
     * @param float  $score
6611
     * @param float  $weight
6612
     *
6613
     * @return bool
6614
     */
6615
    public function send_mail_notification_for_exam(
6616
        $type,
6617
        $question_list_answers,
6618
        $origin,
6619
        $exe_id,
6620
        $score = null,
6621
        $weight = null
6622
    ) {
6623
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6624
6625
        if ((empty($setting) || !is_array($setting)) && empty($this->getNotifications())) {
6626
            return false;
6627
        }
6628
6629
        $settingFromExercise = $this->getNotifications();
6630
        if (!empty($settingFromExercise)) {
6631
            $setting = $settingFromExercise;
6632
        }
6633
6634
        // Email configuration settings
6635
        $courseCode = api_get_course_id();
6636
        $courseInfo = api_get_course_info($courseCode);
6637
6638
        if (empty($courseInfo)) {
6639
            return false;
6640
        }
6641
6642
        $sessionId = api_get_session_id();
6643
6644
        $sessionData = '';
6645
        if (!empty($sessionId)) {
6646
            $sessionInfo = api_get_session_info($sessionId);
6647
            if (!empty($sessionInfo)) {
6648
                $sessionData = '<tr>'
6649
                    .'<td>'.get_lang('Session name').'</td>'
6650
                    .'<td>'.$sessionInfo['name'].'</td>'
6651
                    .'</tr>';
6652
            }
6653
        }
6654
6655
        $sendStart = false;
6656
        $sendEnd = false;
6657
        $sendEndOpenQuestion = false;
6658
        $sendEndOralQuestion = false;
6659
6660
        foreach ($setting as $option) {
6661
            switch ($option) {
6662
                case 0:
6663
                    return false;
6664
6665
                    break;
6666
                case 1: // End
6667
                    if ('end' == $type) {
6668
                        $sendEnd = true;
6669
                    }
6670
6671
                    break;
6672
                case 2: // start
6673
                    if ('start' == $type) {
6674
                        $sendStart = true;
6675
                    }
6676
6677
                    break;
6678
                case 3: // end + open
6679
                    if ('end' == $type) {
6680
                        $sendEndOpenQuestion = true;
6681
                    }
6682
6683
                    break;
6684
                case 4: // end + oral
6685
                    if ('end' == $type) {
6686
                        $sendEndOralQuestion = true;
6687
                    }
6688
6689
                    break;
6690
            }
6691
        }
6692
6693
        $user_info = api_get_user_info(api_get_user_id());
6694
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6695
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6696
6697
        if (!empty($sessionId)) {
6698
            $addGeneralCoach = true;
6699
            $setting = ('true' === api_get_setting('exercise.block_quiz_mail_notification_general_coach'));
6700
            if (true === $setting) {
6701
                $addGeneralCoach = false;
6702
            }
6703
            $teachers = CourseManager::get_coach_list_from_course_code(
6704
                $courseCode,
6705
                $sessionId,
6706
                $addGeneralCoach
6707
            );
6708
        } else {
6709
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6710
        }
6711
6712
        if ($sendEndOpenQuestion) {
6713
            $this->sendNotificationForOpenQuestions(
6714
                $question_list_answers,
6715
                $origin,
6716
                $user_info,
6717
                $url,
6718
                $teachers
6719
            );
6720
        }
6721
6722
        if ($sendEndOralQuestion) {
6723
            $this->sendNotificationForOralQuestions(
6724
                $question_list_answers,
6725
                $origin,
6726
                $exe_id,
6727
                $user_info,
6728
                $url,
6729
                $teachers
6730
            );
6731
        }
6732
6733
        if (!$sendEnd && !$sendStart) {
6734
            return false;
6735
        }
6736
6737
        $scoreLabel = '';
6738
        if ($sendEnd &&
6739
            ('true' === api_get_setting('exercise.send_score_in_exam_notification_mail_to_manager'))
6740
        ) {
6741
            $notificationPercentage = ('true' === api_get_setting('mail.send_notification_score_in_percentage'));
6742
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6743
            $scoreLabel = '<tr>
6744
                            <td>'.get_lang('Score')."</td>
6745
                            <td>&nbsp;$scoreLabel</td>
6746
                        </tr>";
6747
        }
6748
6749
        if ($sendEnd) {
6750
            $msg = get_lang('A learner attempted an exercise').'<br /><br />';
6751
        } else {
6752
            $msg = get_lang('Student just started an exercise').'<br /><br />';
6753
        }
6754
6755
        $msg .= get_lang('Attempt details').' : <br /><br />
6756
                    <table>
6757
                        <tr>
6758
                            <td>'.get_lang('Course name').'</td>
6759
                            <td>#course#</td>
6760
                        </tr>
6761
                        '.$sessionData.'
6762
                        <tr>
6763
                            <td>'.get_lang('Test').'</td>
6764
                            <td>&nbsp;#exercise#</td>
6765
                        </tr>
6766
                        <tr>
6767
                            <td>'.get_lang('Learner name').'</td>
6768
                            <td>&nbsp;#student_complete_name#</td>
6769
                        </tr>
6770
                        <tr>
6771
                            <td>'.get_lang('Learner e-mail').'</td>
6772
                            <td>&nbsp;#email#</td>
6773
                        </tr>
6774
                        '.$scoreLabel.'
6775
                    </table>';
6776
6777
        $variables = [
6778
            '#email#' => $user_info['email'],
6779
            '#exercise#' => $this->exercise,
6780
            '#student_complete_name#' => $user_info['complete_name'],
6781
            '#course#' => Display::url(
6782
                $courseInfo['title'],
6783
                $courseInfo['course_public_url'].'?sid='.$sessionId
6784
            ),
6785
        ];
6786
6787
        if ($sendEnd) {
6788
            $msg .= '<br /><a href="#url#">'.get_lang(
6789
                    'Click this link to check the answer and/or give feedback'
6790
                ).'</a>';
6791
            $variables['#url#'] = $url;
6792
        }
6793
6794
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6795
6796
        if ($sendEnd) {
6797
            $subject = get_lang('A learner attempted an exercise');
6798
        } else {
6799
            $subject = get_lang('Student just started an exercise');
6800
        }
6801
6802
        if (!empty($teachers)) {
6803
            foreach ($teachers as $user_id => $teacher_data) {
6804
                MessageManager::send_message_simple(
6805
                    $user_id,
6806
                    $subject,
6807
                    $content
6808
                );
6809
            }
6810
        }
6811
    }
6812
6813
    /**
6814
     * @param array $user_data         result of api_get_user_info()
6815
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6816
     * @param bool  $saveUserResult
6817
     * @param bool  $allowSignature
6818
     * @param bool  $allowExportPdf
6819
     *
6820
     * @return string
6821
     */
6822
    public function showExerciseResultHeader(
6823
        $user_data,
6824
        $trackExerciseInfo,
6825
        $saveUserResult,
6826
        $allowSignature = false,
6827
        $allowExportPdf = false
6828
    ) {
6829
        if ('true' === api_get_setting('exercise.hide_user_info_in_quiz_result')) {
6830
            return '';
6831
        }
6832
6833
        $start_date = null;
6834
        if (isset($trackExerciseInfo['start_date'])) {
6835
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6836
        }
6837
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6838
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6839
6840
        if (!empty($user_data)) {
6841
            $userFullName = $user_data['complete_name'];
6842
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6843
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('Go to learner details').'">'.
6844
                    $user_data['complete_name'].'</a>';
6845
            }
6846
6847
            $data = [
6848
                'name_url' => $userFullName,
6849
                'complete_name' => $user_data['complete_name'],
6850
                'username' => $user_data['username'],
6851
                'avatar' => $user_data['avatar_medium'],
6852
                'url' => $user_data['profile_url'],
6853
            ];
6854
6855
            if (!empty($user_data['official_code'])) {
6856
                $data['code'] = $user_data['official_code'];
6857
            }
6858
        }
6859
        // Description can be very long and is generally meant to explain
6860
        //   rules *before* the exam. Leaving here to make display easier if
6861
        //   necessary
6862
        /*
6863
        if (!empty($this->description)) {
6864
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6865
        }
6866
        */
6867
6868
        $data['start_date'] = $start_date;
6869
        $data['duration'] = $duration;
6870
        $data['ip'] = $ip;
6871
6872
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
6873
            $data['title'] = $this->get_formated_title().get_lang('Result');
6874
        } else {
6875
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6876
        }
6877
6878
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6879
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6880
6881
        $data['number_of_answers'] = $questionsCount;
6882
        $data['number_of_answers_saved'] = $savedAnswersCount;
6883
        $exeId = $trackExerciseInfo['exe_id'];
6884
6885
        if ('true' === api_get_setting('exercise.quiz_confirm_saved_answers')) {
6886
            $em = Database::getManager();
6887
6888
            if ($saveUserResult) {
6889
                $trackConfirmation = new TrackEExerciseConfirmation();
6890
                $trackConfirmation
6891
                    ->setUser(api_get_user_entity($trackExerciseInfo['exe_user_id']))
6892
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6893
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6894
                    ->setQuestionsCount($questionsCount)
6895
                    ->setSavedAnswersCount($savedAnswersCount)
6896
                    ->setCourseId($trackExerciseInfo['c_id'])
6897
                    ->setSessionId($trackExerciseInfo['session_id'])
6898
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6899
6900
                $em->persist($trackConfirmation);
6901
                $em->flush();
6902
            } else {
6903
                $trackConfirmation = $em
6904
                    ->getRepository(TrackEExerciseConfirmation::class)
6905
                    ->findOneBy(
6906
                        [
6907
                            'attemptId' => $trackExerciseInfo['exe_id'],
6908
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6909
                            'courseId' => $trackExerciseInfo['c_id'],
6910
                            'sessionId' => $trackExerciseInfo['session_id'],
6911
                        ]
6912
                    );
6913
            }
6914
6915
            $data['track_confirmation'] = $trackConfirmation;
6916
        }
6917
6918
        $signature = '';
6919
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6920
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6921
        }
6922
        $tpl = new Template(null, false, false, false, false, false, false);
6923
        $tpl->assign('data', $data);
6924
        $tpl->assign('allow_signature', $allowSignature);
6925
        $tpl->assign('signature', $signature);
6926
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6927
        $tpl->assign(
6928
            'export_url',
6929
            api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq()
6930
        );
6931
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6932
6933
        return $tpl->fetch($layoutTemplate);
6934
    }
6935
6936
    /**
6937
     * Returns the exercise result.
6938
     *
6939
     * @param int        attempt id
6940
     *
6941
     * @return array
6942
     */
6943
    public function get_exercise_result($exe_id)
6944
    {
6945
        $result = [];
6946
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6947
6948
        if (!empty($track_exercise_info)) {
6949
            $totalScore = 0;
6950
            $objExercise = new self();
6951
            $objExercise->read($track_exercise_info['exe_exo_id']);
6952
            if (!empty($track_exercise_info['data_tracking'])) {
6953
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6954
            }
6955
            foreach ($question_list as $questionId) {
6956
                $question_result = $objExercise->manage_answer(
6957
                    $exe_id,
6958
                    $questionId,
6959
                    '',
6960
                    'exercise_show',
6961
                    [],
6962
                    false,
6963
                    true,
6964
                    false,
6965
                    $objExercise->selectPropagateNeg()
6966
                );
6967
                $totalScore += $question_result['score'];
6968
            }
6969
6970
            if (0 == $objExercise->selectPropagateNeg() && $totalScore < 0) {
6971
                $totalScore = 0;
6972
            }
6973
            $result = [
6974
                'score' => $totalScore,
6975
                'weight' => $track_exercise_info['max_score'],
6976
            ];
6977
        }
6978
6979
        return $result;
6980
    }
6981
6982
    /**
6983
     * Checks if the exercise is visible due a lot of conditions
6984
     * visibility, time limits, student attempts
6985
     * Return associative array
6986
     * value : true if exercise visible
6987
     * message : HTML formatted message
6988
     * rawMessage : text message.
6989
     *
6990
     * @param int  $lpId
6991
     * @param int  $lpItemId
6992
     * @param int  $lpItemViewId
6993
     * @param bool $filterByAdmin
6994
     *
6995
     * @return array
6996
     */
6997
    public function is_visible(
6998
        $lpId = 0,
6999
        $lpItemId = 0,
7000
        $lpItemViewId = 0,
7001
        $filterByAdmin = true
7002
    ) {
7003
        // 1. By default the exercise is visible
7004
        $isVisible = true;
7005
        $message = null;
7006
7007
        // 1.1 Admins and teachers can access to the exercise
7008
        if ($filterByAdmin) {
7009
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
7010
                return ['value' => true, 'message' => ''];
7011
            }
7012
        }
7013
7014
        // Deleted exercise.
7015
        if (-1 == $this->active) {
7016
            return [
7017
                'value' => false,
7018
                'message' => Display::return_message(
7019
                    get_lang('Test not found or not visible'),
7020
                    'warning',
7021
                    false
7022
                ),
7023
                'rawMessage' => get_lang('Test not found or not visible'),
7024
            ];
7025
        }
7026
7027
        $repo = Container::getQuizRepository();
7028
        $exercise = $repo->find($this->iId);
7029
7030
        if (null === $exercise) {
7031
            return [];
7032
        }
7033
7034
        $course = api_get_course_entity($this->course_id);
7035
        $link = $exercise->getFirstResourceLinkFromCourseSession($course);
7036
7037
        if ($link && $link->isDraft()) {
7038
            $this->active = 0;
7039
        }
7040
7041
        // 2. If the exercise is not active.
7042
        if (empty($lpId)) {
7043
            // 2.1 LP is OFF
7044
            if (0 == $this->active) {
7045
                return [
7046
                    'value' => false,
7047
                    'message' => Display::return_message(
7048
                        get_lang('Test not found or not visible'),
7049
                        'warning',
7050
                        false
7051
                    ),
7052
                    'rawMessage' => get_lang('Test not found or not visible'),
7053
                ];
7054
            }
7055
        } else {
7056
            $lp = Container::getLpRepository()->find($lpId);
7057
            // 2.1 LP is loaded
7058
            if ($lp && 0 == $this->active &&
7059
                !learnpath::is_lp_visible_for_student($lp, api_get_user_id(), $course)
7060
            ) {
7061
                return [
7062
                    'value' => false,
7063
                    'message' => Display::return_message(
7064
                        get_lang('Test not found or not visible'),
7065
                        'warning',
7066
                        false
7067
                    ),
7068
                    'rawMessage' => get_lang('Test not found or not visible'),
7069
                ];
7070
            }
7071
        }
7072
7073
        // 3. We check if the time limits are on
7074
        $limitTimeExists = false;
7075
        if (!empty($this->start_time) || !empty($this->end_time)) {
7076
            $limitTimeExists = true;
7077
        }
7078
7079
        if ($limitTimeExists) {
7080
            $timeNow = time();
7081
            $existsStartDate = false;
7082
            $nowIsAfterStartDate = true;
7083
            $existsEndDate = false;
7084
            $nowIsBeforeEndDate = true;
7085
7086
            if (!empty($this->start_time)) {
7087
                $existsStartDate = true;
7088
            }
7089
7090
            if (!empty($this->end_time)) {
7091
                $existsEndDate = true;
7092
            }
7093
7094
            // check if we are before-or-after end-or-start date
7095
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
7096
                $nowIsAfterStartDate = false;
7097
            }
7098
7099
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
7100
                $nowIsBeforeEndDate = false;
7101
            }
7102
7103
            // lets check all cases
7104
            if ($existsStartDate && !$existsEndDate) {
7105
                // exists start date and dont exists end date
7106
                if ($nowIsAfterStartDate) {
7107
                    // after start date, no end date
7108
                    $isVisible = true;
7109
                    $message = sprintf(
7110
                        get_lang('Exercise available since %s'),
7111
                        api_convert_and_format_date($this->start_time)
7112
                    );
7113
                } else {
7114
                    // before start date, no end date
7115
                    $isVisible = false;
7116
                    $message = sprintf(
7117
                        get_lang('Exercise available from %s'),
7118
                        api_convert_and_format_date($this->start_time)
7119
                    );
7120
                }
7121
            } elseif (!$existsStartDate && $existsEndDate) {
7122
                // doesnt exist start date, exists end date
7123
                if ($nowIsBeforeEndDate) {
7124
                    // before end date, no start date
7125
                    $isVisible = true;
7126
                    $message = sprintf(
7127
                        get_lang('Exercise available until %s'),
7128
                        api_convert_and_format_date($this->end_time)
7129
                    );
7130
                } else {
7131
                    // after end date, no start date
7132
                    $isVisible = false;
7133
                    $message = sprintf(
7134
                        get_lang('Exercise available until %s'),
7135
                        api_convert_and_format_date($this->end_time)
7136
                    );
7137
                }
7138
            } elseif ($existsStartDate && $existsEndDate) {
7139
                // exists start date and end date
7140
                if ($nowIsAfterStartDate) {
7141
                    if ($nowIsBeforeEndDate) {
7142
                        // after start date and before end date
7143
                        $isVisible = true;
7144
                        $message = sprintf(
7145
                            get_lang('Exercise was activated from %s to %s'),
7146
                            api_convert_and_format_date($this->start_time),
7147
                            api_convert_and_format_date($this->end_time)
7148
                        );
7149
                    } else {
7150
                        // after start date and after end date
7151
                        $isVisible = false;
7152
                        $message = sprintf(
7153
                            get_lang('Exercise was activated from %s to %s'),
7154
                            api_convert_and_format_date($this->start_time),
7155
                            api_convert_and_format_date($this->end_time)
7156
                        );
7157
                    }
7158
                } else {
7159
                    if ($nowIsBeforeEndDate) {
7160
                        // before start date and before end date
7161
                        $isVisible = false;
7162
                        $message = sprintf(
7163
                            get_lang('Exercise will be activated from %s to %s'),
7164
                            api_convert_and_format_date($this->start_time),
7165
                            api_convert_and_format_date($this->end_time)
7166
                        );
7167
                    }
7168
                    // case before start date and after end date is impossible
7169
                }
7170
            } elseif (!$existsStartDate && !$existsEndDate) {
7171
                // doesnt exist start date nor end date
7172
                $isVisible = true;
7173
                $message = '';
7174
            }
7175
        }
7176
7177
        // 4. We check if the student have attempts
7178
        if ($isVisible) {
7179
            $exerciseAttempts = $this->selectAttempts();
7180
7181
            if ($exerciseAttempts > 0) {
7182
                $attemptCount = Event::get_attempt_count(
7183
                    api_get_user_id(),
7184
                    $this->getId(),
7185
                    (int) $lpId,
7186
                    (int) $lpItemId,
7187
                    (int) $lpItemViewId
7188
                );
7189
7190
                if ($attemptCount >= $exerciseAttempts) {
7191
                    $message = sprintf(
7192
                        get_lang('You cannot take test <b>%s</b> because you have already reached the maximum of %s attempts.'),
7193
                        $this->name,
7194
                        $exerciseAttempts
7195
                    );
7196
                    $isVisible = false;
7197
                } else {
7198
                    // Check blocking exercise.
7199
                    $extraFieldValue = new ExtraFieldValue('exercise');
7200
                    $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
7201
                        $this->iId,
7202
                        'blocking_percentage'
7203
                    );
7204
                    if ($blockExercise && isset($blockExercise['value']) && !empty($blockExercise['value'])) {
7205
                        $blockPercentage = (int) $blockExercise['value'];
7206
                        $userAttempts = Event::getExerciseResultsByUser(
7207
                            api_get_user_id(),
7208
                            $this->iId,
7209
                            $this->course_id,
7210
                            $this->sessionId,
7211
                            $lpId,
7212
                            $lpItemId
7213
                        );
7214
7215
                        if (!empty($userAttempts)) {
7216
                            $currentAttempt = current($userAttempts);
7217
                            if ($currentAttempt['total_percentage'] <= $blockPercentage) {
7218
                                $message = sprintf(
7219
                                    get_lang('All attempts blocked because you did not reach the minimum score of %s % at one of your attempts.'),
7220
                                    $blockPercentage
7221
                                );
7222
                                $isVisible = false;
7223
                            }
7224
                        }
7225
                    }
7226
                }
7227
            }
7228
        }
7229
7230
        $rawMessage = '';
7231
        if (!empty($message)) {
7232
            $rawMessage = $message;
7233
            $message = Display::return_message($message, 'warning', false);
7234
        }
7235
7236
        return [
7237
            'value' => $isVisible,
7238
            'message' => $message,
7239
            'rawMessage' => $rawMessage,
7240
        ];
7241
    }
7242
7243
    /**
7244
     * @return bool
7245
     */
7246
    public function added_in_lp()
7247
    {
7248
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
7249
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
7250
                WHERE
7251
                    item_type = '".TOOL_QUIZ."' AND
7252
                    path = '{$this->getId()}'";
7253
        $result = Database::query($sql);
7254
        if (Database::num_rows($result) > 0) {
7255
            return true;
7256
        }
7257
7258
        return false;
7259
    }
7260
7261
    /**
7262
     * Returns an array with this form.
7263
     *
7264
     * @return array
7265
     *
7266
     * @example
7267
     * <code>
7268
     * array (size=3)
7269
     * 999 =>
7270
     * array (size=3)
7271
     * 0 => int 3422
7272
     * 1 => int 3423
7273
     * 2 => int 3424
7274
     * 100 =>
7275
     * array (size=2)
7276
     * 0 => int 3469
7277
     * 1 => int 3470
7278
     * 101 =>
7279
     * array (size=1)
7280
     * 0 => int 3482
7281
     * </code>
7282
     * The array inside the key 999 means the question list that belongs to the media id = 999,
7283
     * this case is special because 999 means "no media".
7284
     */
7285
    public function getMediaList()
7286
    {
7287
        return $this->mediaList;
7288
    }
7289
7290
    /**
7291
     * Is media question activated?
7292
     *
7293
     * @return bool
7294
     */
7295
    public function mediaIsActivated()
7296
    {
7297
        $mediaQuestions = $this->getMediaList();
7298
        $active = false;
7299
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
7300
            $media_count = count($mediaQuestions);
7301
            if ($media_count > 1) {
7302
                return true;
7303
            } elseif (1 == $media_count) {
7304
                if (isset($mediaQuestions[999])) {
7305
                    return false;
7306
                } else {
7307
                    return true;
7308
                }
7309
            }
7310
        }
7311
7312
        return $active;
7313
    }
7314
7315
    /**
7316
     * Gets question list from the exercise.
7317
     *
7318
     * @return array
7319
     */
7320
    public function getQuestionList()
7321
    {
7322
        return $this->questionList;
7323
    }
7324
7325
    /**
7326
     * Question list with medias compressed like this.
7327
     *
7328
     * @return array
7329
     *
7330
     * @example
7331
     *      <code>
7332
     *      array(
7333
     *      question_id_1,
7334
     *      question_id_2,
7335
     *      media_id, <- this media id contains question ids
7336
     *      question_id_3,
7337
     *      )
7338
     *      </code>
7339
     */
7340
    public function getQuestionListWithMediasCompressed()
7341
    {
7342
        return $this->questionList;
7343
    }
7344
7345
    /**
7346
     * Question list with medias uncompressed like this.
7347
     *
7348
     * @return array
7349
     *
7350
     * @example
7351
     *      <code>
7352
     *      array(
7353
     *      question_id,
7354
     *      question_id,
7355
     *      question_id, <- belongs to a media id
7356
     *      question_id, <- belongs to a media id
7357
     *      question_id,
7358
     *      )
7359
     *      </code>
7360
     */
7361
    public function getQuestionListWithMediasUncompressed()
7362
    {
7363
        return $this->questionListUncompressed;
7364
    }
7365
7366
    /**
7367
     * Sets the question list when the exercise->read() is executed.
7368
     *
7369
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
7370
     */
7371
    public function setQuestionList($adminView = false)
7372
    {
7373
        // Getting question list.
7374
        $questionList = $this->selectQuestionList(true, $adminView);
7375
        $this->setMediaList($questionList);
7376
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
7377
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
7378
            $questionList,
7379
            true
7380
        );
7381
    }
7382
7383
    /**
7384
     * @params array question list
7385
     * @params bool expand or not question list (true show all questions,
7386
     * false show media question id instead of the question ids)
7387
     */
7388
    public function transformQuestionListWithMedias(
7389
        $question_list,
7390
        $expand_media_questions = false
7391
    ) {
7392
        $new_question_list = [];
7393
        if (!empty($question_list)) {
7394
            $media_questions = $this->getMediaList();
7395
            $media_active = $this->mediaIsActivated($media_questions);
7396
7397
            if ($media_active) {
7398
                $counter = 1;
7399
                foreach ($question_list as $question_id) {
7400
                    $add_question = true;
7401
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7402
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7403
                            $add_question = false;
7404
                            if (!in_array($media_id, $new_question_list)) {
7405
                                $new_question_list[$counter] = $media_id;
7406
                                $counter++;
7407
                            }
7408
7409
                            break;
7410
                        }
7411
                    }
7412
                    if ($add_question) {
7413
                        $new_question_list[$counter] = $question_id;
7414
                        $counter++;
7415
                    }
7416
                }
7417
                if ($expand_media_questions) {
7418
                    $media_key_list = array_keys($media_questions);
7419
                    foreach ($new_question_list as &$question_id) {
7420
                        if (in_array($question_id, $media_key_list)) {
7421
                            $question_id = $media_questions[$question_id];
7422
                        }
7423
                    }
7424
                    $new_question_list = array_flatten($new_question_list);
7425
                }
7426
            } else {
7427
                $new_question_list = $question_list;
7428
            }
7429
        }
7430
7431
        return $new_question_list;
7432
    }
7433
7434
    /**
7435
     * Get question list depend on the random settings.
7436
     *
7437
     * @return array
7438
     */
7439
    public function get_validated_question_list()
7440
    {
7441
        $isRandomByCategory = $this->isRandomByCat();
7442
        if (0 == $isRandomByCategory) {
7443
            if ($this->isRandom()) {
7444
                return $this->getRandomList();
7445
            }
7446
7447
            return $this->selectQuestionList();
7448
        }
7449
7450
        if ($this->isRandom()) {
7451
            // USE question categories
7452
            // get questions by category for this exercise
7453
            // we have to choice $objExercise->random question in each array values of $tabCategoryQuestions
7454
            // key of $tabCategoryQuestions are the categopy id (0 for not in a category)
7455
            // value is the array of question id of this category
7456
            $questionList = [];
7457
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
7458
            $isRandomByCategory = $this->getRandomByCategory();
7459
            // We sort categories based on the term between [] in the head
7460
            // of the category's description
7461
            /* examples of categories :
7462
             * [biologie] Maitriser les mecanismes de base de la genetique
7463
             * [biologie] Relier les moyens de depenses et les agents infectieux
7464
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
7465
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
7466
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
7467
             * [chimie] Connaître les charges des particules
7468
             * We want that in the order of the groups defined by the term
7469
             * between brackets at the beginning of the category title
7470
            */
7471
            // If test option is Grouped By Categories
7472
            if (2 == $isRandomByCategory) {
7473
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
7474
            }
7475
            foreach ($categoryQuestions as $question) {
7476
                $number_of_random_question = $this->random;
7477
                if (-1 == $this->random) {
7478
                    $number_of_random_question = count($this->questionList);
7479
                }
7480
                $questionList = array_merge(
7481
                    $questionList,
7482
                    TestCategory::getNElementsFromArray(
7483
                        $question,
7484
                        $number_of_random_question
7485
                    )
7486
                );
7487
            }
7488
            // shuffle the question list if test is not grouped by categories
7489
            if (1 == $isRandomByCategory) {
7490
                shuffle($questionList); // or not
7491
            }
7492
7493
            return $questionList;
7494
        }
7495
7496
        // Problem, random by category has been selected and
7497
        // we have no $this->isRandom number of question selected
7498
        // Should not happened
7499
7500
        return [];
7501
    }
7502
7503
    public function get_question_list($expand_media_questions = false)
7504
    {
7505
        $question_list = $this->get_validated_question_list();
7506
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
7507
7508
        return $question_list;
7509
    }
7510
7511
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
7512
    {
7513
        $new_question_list = [];
7514
        if (!empty($question_list)) {
7515
            $media_questions = $this->getMediaList();
7516
            $media_active = $this->mediaIsActivated($media_questions);
7517
7518
            if ($media_active) {
7519
                $counter = 1;
7520
                foreach ($question_list as $question_id) {
7521
                    $add_question = true;
7522
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7523
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7524
                            $add_question = false;
7525
                            if (!in_array($media_id, $new_question_list)) {
7526
                                $new_question_list[$counter] = $media_id;
7527
                                $counter++;
7528
                            }
7529
7530
                            break;
7531
                        }
7532
                    }
7533
                    if ($add_question) {
7534
                        $new_question_list[$counter] = $question_id;
7535
                        $counter++;
7536
                    }
7537
                }
7538
                if ($expand_media_questions) {
7539
                    $media_key_list = array_keys($media_questions);
7540
                    foreach ($new_question_list as &$question_id) {
7541
                        if (in_array($question_id, $media_key_list)) {
7542
                            $question_id = $media_questions[$question_id];
7543
                        }
7544
                    }
7545
                    $new_question_list = array_flatten($new_question_list);
7546
                }
7547
            } else {
7548
                $new_question_list = $question_list;
7549
            }
7550
        }
7551
7552
        return $new_question_list;
7553
    }
7554
7555
    /**
7556
     * @param int $exe_id
7557
     *
7558
     * @return array
7559
     */
7560
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7561
    {
7562
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7563
        $exe_id = (int) $exe_id;
7564
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7565
        $result = Database::query($sql_track);
7566
        $new_array = [];
7567
        if (Database::num_rows($result) > 0) {
7568
            $new_array = Database::fetch_assoc($result);
7569
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7570
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7571
            $new_array['duration_formatted'] = '';
7572
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7573
                $time = api_format_time($new_array['exe_duration'], 'js');
7574
                $new_array['duration_formatted'] = $time;
7575
            }
7576
        }
7577
7578
        return $new_array;
7579
    }
7580
7581
    /**
7582
     * @param int $exeId
7583
     *
7584
     * @return bool
7585
     */
7586
    public function removeAllQuestionToRemind($exeId)
7587
    {
7588
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7589
        $exeId = (int) $exeId;
7590
        if (empty($exeId)) {
7591
            return false;
7592
        }
7593
        $sql = "UPDATE $table
7594
                SET questions_to_check = ''
7595
                WHERE exe_id = $exeId ";
7596
        Database::query($sql);
7597
7598
        return true;
7599
    }
7600
7601
    /**
7602
     * @param int   $exeId
7603
     * @param array $questionList
7604
     *
7605
     * @return bool
7606
     */
7607
    public function addAllQuestionToRemind($exeId, $questionList = [])
7608
    {
7609
        $exeId = (int) $exeId;
7610
        if (empty($questionList)) {
7611
            return false;
7612
        }
7613
7614
        $questionListToString = implode(',', $questionList);
7615
        $questionListToString = Database::escape_string($questionListToString);
7616
7617
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7618
        $sql = "UPDATE $table
7619
                SET questions_to_check = '$questionListToString'
7620
                WHERE exe_id = $exeId";
7621
        Database::query($sql);
7622
7623
        return true;
7624
    }
7625
7626
    /**
7627
     * @param int    $exeId
7628
     * @param int    $questionId
7629
     * @param string $action
7630
     */
7631
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7632
    {
7633
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7634
        $questionId = (int) $questionId;
7635
        $exeId = (int) $exeId;
7636
7637
        if ($exercise_info) {
7638
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7639
            if (empty($exercise_info['questions_to_check'])) {
7640
                if ('add' == $action) {
7641
                    $sql = "UPDATE $track_exercises
7642
                            SET questions_to_check = '$questionId'
7643
                            WHERE exe_id = $exeId ";
7644
                    Database::query($sql);
7645
                }
7646
            } else {
7647
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7648
                $remind_list_string = '';
7649
                if ('add' === $action) {
7650
                    if (!in_array($questionId, $remind_list)) {
7651
                        $newRemindList = [];
7652
                        $remind_list[] = $questionId;
7653
                        $questionListInSession = Session::read('questionList');
7654
                        if (!empty($questionListInSession)) {
7655
                            foreach ($questionListInSession as $originalQuestionId) {
7656
                                if (in_array($originalQuestionId, $remind_list)) {
7657
                                    $newRemindList[] = $originalQuestionId;
7658
                                }
7659
                            }
7660
                        }
7661
                        $remind_list_string = implode(',', $newRemindList);
7662
                    }
7663
                } elseif ('delete' == $action) {
7664
                    if (!empty($remind_list)) {
7665
                        if (in_array($questionId, $remind_list)) {
7666
                            $remind_list = array_flip($remind_list);
7667
                            unset($remind_list[$questionId]);
7668
                            $remind_list = array_flip($remind_list);
7669
7670
                            if (!empty($remind_list)) {
7671
                                sort($remind_list);
7672
                                array_filter($remind_list);
7673
                                $remind_list_string = implode(',', $remind_list);
7674
                            }
7675
                        }
7676
                    }
7677
                }
7678
                $value = Database::escape_string($remind_list_string);
7679
                $sql = "UPDATE $track_exercises
7680
                        SET questions_to_check = '$value'
7681
                        WHERE exe_id = $exeId ";
7682
                Database::query($sql);
7683
            }
7684
        }
7685
    }
7686
7687
    /**
7688
     * @param string $answer
7689
     */
7690
    public function fill_in_blank_answer_to_array($answer)
7691
    {
7692
        $list = null;
7693
        api_preg_match_all('/\[[^]]+\]/', $answer, $list);
7694
7695
        if (empty($list)) {
7696
            return '';
7697
        }
7698
7699
        return $list[0];
7700
    }
7701
7702
    /**
7703
     * @param string $answer
7704
     *
7705
     * @return string
7706
     */
7707
    public function fill_in_blank_answer_to_string($answer)
7708
    {
7709
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7710
        $result = '';
7711
        if (!empty($teacher_answer_list)) {
7712
            foreach ($teacher_answer_list as $teacher_item) {
7713
                //Cleaning student answer list
7714
                $value = strip_tags($teacher_item);
7715
                $value = api_substr($value, 1, api_strlen($value) - 2);
7716
                $value = explode('/', $value);
7717
                if (!empty($value[0])) {
7718
                    $value = trim($value[0]);
7719
                    $value = str_replace('&nbsp;', '', $value);
7720
                    $result .= $value;
7721
                }
7722
            }
7723
        }
7724
7725
        return $result;
7726
    }
7727
7728
    /**
7729
     * @return string
7730
     */
7731
    public function returnTimeLeftDiv()
7732
    {
7733
        $html = '<div id="clock_warning" style="display:none">';
7734
        $html .= Display::return_message(
7735
            get_lang('Time limit reached'),
7736
            'warning'
7737
        );
7738
        $html .= ' ';
7739
        $html .= sprintf(
7740
            get_lang('Just a moment, please. You will be redirected in %s seconds...'),
7741
            '<span id="counter_to_redirect" class="red_alert"></span>'
7742
        );
7743
        $html .= '</div>';
7744
        $icon = Display::getMdiIcon('clock-outline', 'ch-tool-icon');
7745
        $html .= '<div class="count_down">
7746
                    '.get_lang('Remaining time to finish exercise').'
7747
                    '.$icon.'<span id="exercise_clock_warning"></span>
7748
                </div>';
7749
7750
        return $html;
7751
    }
7752
7753
    /**
7754
     * Get categories added in the exercise--category matrix.
7755
     *
7756
     * @return array
7757
     */
7758
    public function getCategoriesInExercise()
7759
    {
7760
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7761
        if (!empty($this->getId())) {
7762
            $sql = "SELECT * FROM $table
7763
                    WHERE exercise_id = {$this->getId()} ";
7764
            $result = Database::query($sql);
7765
            $list = [];
7766
            if (Database::num_rows($result)) {
7767
                while ($row = Database::fetch_assoc($result)) {
7768
                    $list[$row['category_id']] = $row;
7769
                }
7770
7771
                return $list;
7772
            }
7773
        }
7774
7775
        return [];
7776
    }
7777
7778
    /**
7779
     * Get total number of question that will be parsed when using the category/exercise.
7780
     *
7781
     * @return int
7782
     */
7783
    public function getNumberQuestionExerciseCategory()
7784
    {
7785
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7786
        if (!empty($this->getId())) {
7787
            $sql = "SELECT SUM(count_questions) count_questions
7788
                    FROM $table
7789
                    WHERE exercise_id = {$this->getId()}";
7790
            $result = Database::query($sql);
7791
            if (Database::num_rows($result)) {
7792
                $row = Database::fetch_array($result);
7793
7794
                return (int) $row['count_questions'];
7795
            }
7796
        }
7797
7798
        return 0;
7799
    }
7800
7801
    /**
7802
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table
7803
     *
7804
     * @param array $categories
7805
     */
7806
    public function saveCategoriesInExercise($categories)
7807
    {
7808
        if (!empty($categories) && !empty($this->getId())) {
7809
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7810
            $sql = "DELETE FROM $table
7811
                    WHERE exercise_id = {$this->getId()}";
7812
            Database::query($sql);
7813
            foreach ($categories as $categoryId => $countQuestions) {
7814
                if ($categoryId !== 0) {
7815
                    $params = [
7816
                        'exercise_id' => $this->getId(),
7817
                        'category_id' => $categoryId,
7818
                        'count_questions' => $countQuestions,
7819
                    ];
7820
                    Database::insert($table, $params);
7821
                }
7822
            }
7823
        }
7824
    }
7825
7826
    /**
7827
     * @param array  $questionList
7828
     * @param int    $currentQuestion
7829
     * @param array  $conditions
7830
     * @param string $link
7831
     *
7832
     * @return string
7833
     */
7834
    public function progressExercisePaginationBar(
7835
        $questionList,
7836
        $currentQuestion,
7837
        $conditions,
7838
        $link
7839
    ) {
7840
        $mediaQuestions = $this->getMediaList();
7841
7842
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7843
        $counter = 0;
7844
        $nextValue = 0;
7845
        $wasMedia = false;
7846
        $before = 0;
7847
        $counterNoMedias = 0;
7848
        foreach ($questionList as $questionId) {
7849
            $isCurrent = $currentQuestion == $counterNoMedias + 1 ? true : false;
7850
7851
            if (!empty($nextValue)) {
7852
                if ($wasMedia) {
7853
                    $nextValue = $nextValue - $before + 1;
7854
                }
7855
            }
7856
7857
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7858
                $fixedValue = $counterNoMedias;
7859
7860
                $html .= Display::progressPaginationBar(
7861
                    $nextValue,
7862
                    $mediaQuestions[$questionId],
7863
                    $currentQuestion,
7864
                    $fixedValue,
7865
                    $conditions,
7866
                    $link,
7867
                    true,
7868
                    true
7869
                );
7870
7871
                $counter += count($mediaQuestions[$questionId]) - 1;
7872
                $before = count($questionList);
7873
                $wasMedia = true;
7874
                $nextValue += count($questionList);
7875
            } else {
7876
                $html .= Display::parsePaginationItem(
7877
                    $questionId,
7878
                    $isCurrent,
7879
                    $conditions,
7880
                    $link,
7881
                    $counter
7882
                );
7883
                $counter++;
7884
                $nextValue++;
7885
                $wasMedia = false;
7886
            }
7887
            $counterNoMedias++;
7888
        }
7889
        $html .= '</ul></div>';
7890
7891
        return $html;
7892
    }
7893
7894
    /**
7895
     *  Shows a list of numbers that represents the question to answer in a exercise.
7896
     *
7897
     * @param array  $categories
7898
     * @param int    $current
7899
     * @param array  $conditions
7900
     * @param string $link
7901
     *
7902
     * @return string
7903
     */
7904
    public function progressExercisePaginationBarWithCategories(
7905
        $categories,
7906
        $current,
7907
        $conditions = [],
7908
        $link = null
7909
    ) {
7910
        $html = null;
7911
        $counterNoMedias = 0;
7912
        $nextValue = 0;
7913
        $wasMedia = false;
7914
        $before = 0;
7915
7916
        if (!empty($categories)) {
7917
            $selectionType = $this->getQuestionSelectionType();
7918
            $useRootAsCategoryTitle = false;
7919
7920
            // Grouping questions per parent category see BT#6540
7921
            if (in_array(
7922
                $selectionType,
7923
                [
7924
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7925
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7926
                ]
7927
            )) {
7928
                $useRootAsCategoryTitle = true;
7929
            }
7930
7931
            // If the exercise is set to only show the titles of the categories
7932
            // at the root of the tree, then pre-order the categories tree by
7933
            // removing children and summing their questions into the parent
7934
            // categories
7935
            if ($useRootAsCategoryTitle) {
7936
                // The new categories list starts empty
7937
                $newCategoryList = [];
7938
                foreach ($categories as $category) {
7939
                    $rootElement = $category['root'];
7940
7941
                    if (isset($category['parent_info'])) {
7942
                        $rootElement = $category['parent_info']['id'];
7943
                    }
7944
7945
                    //$rootElement = $category['id'];
7946
                    // If the current category's ancestor was never seen
7947
                    // before, then declare it and assign the current
7948
                    // category to it.
7949
                    if (!isset($newCategoryList[$rootElement])) {
7950
                        $newCategoryList[$rootElement] = $category;
7951
                    } else {
7952
                        // If it was already seen, then merge the previous with
7953
                        // the current category
7954
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7955
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7956
                        $newCategoryList[$rootElement] = $category;
7957
                    }
7958
                }
7959
                // Now use the newly built categories list, with only parents
7960
                $categories = $newCategoryList;
7961
            }
7962
7963
            foreach ($categories as $category) {
7964
                $questionList = $category['question_list'];
7965
                // Check if in this category there questions added in a media
7966
                $mediaQuestionId = $category['media_question'];
7967
                $isMedia = false;
7968
                $fixedValue = null;
7969
7970
                // Media exists!
7971
                if (999 != $mediaQuestionId) {
7972
                    $isMedia = true;
7973
                    $fixedValue = $counterNoMedias;
7974
                }
7975
7976
                //$categoryName = $category['path']; << show the path
7977
                $categoryName = $category['name'];
7978
7979
                if ($useRootAsCategoryTitle) {
7980
                    if (isset($category['parent_info'])) {
7981
                        $categoryName = $category['parent_info']['title'];
7982
                    }
7983
                }
7984
                $html .= '<div class="row">';
7985
                $html .= '<div class="span2">'.$categoryName.'</div>';
7986
                $html .= '<div class="span8">';
7987
7988
                if (!empty($nextValue)) {
7989
                    if ($wasMedia) {
7990
                        $nextValue = $nextValue - $before + 1;
7991
                    }
7992
                }
7993
                $html .= Display::progressPaginationBar(
7994
                    $nextValue,
7995
                    $questionList,
7996
                    $current,
7997
                    $fixedValue,
7998
                    $conditions,
7999
                    $link,
8000
                    $isMedia,
8001
                    true
8002
                );
8003
                $html .= '</div>';
8004
                $html .= '</div>';
8005
8006
                if (999 == $mediaQuestionId) {
8007
                    $counterNoMedias += count($questionList);
8008
                } else {
8009
                    $counterNoMedias++;
8010
                }
8011
8012
                $nextValue += count($questionList);
8013
                $before = count($questionList);
8014
8015
                if (999 != $mediaQuestionId) {
8016
                    $wasMedia = true;
8017
                } else {
8018
                    $wasMedia = false;
8019
                }
8020
            }
8021
        }
8022
8023
        return $html;
8024
    }
8025
8026
    /**
8027
     * Renders a question list.
8028
     *
8029
     * @param array $questionList    (with media questions compressed)
8030
     * @param int   $currentQuestion
8031
     * @param array $exerciseResult
8032
     * @param array $attemptList
8033
     * @param array $remindList
8034
     */
8035
    public function renderQuestionList(
8036
        $questionList,
8037
        $currentQuestion,
8038
        $exerciseResult,
8039
        $attemptList,
8040
        $remindList
8041
    ) {
8042
        $mediaQuestions = $this->getMediaList();
8043
        $i = 0;
8044
8045
        // Normal question list render (medias compressed)
8046
        foreach ($questionList as $questionId) {
8047
            $i++;
8048
            // For sequential exercises
8049
8050
            if (ONE_PER_PAGE == $this->type) {
8051
                // If it is not the right question, goes to the next loop iteration
8052
                if ($currentQuestion != $i) {
8053
                    continue;
8054
                } else {
8055
                    if (!in_array(
8056
                        $this->getFeedbackType(),
8057
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
8058
                    )) {
8059
                        // if the user has already answered this question
8060
                        if (isset($exerciseResult[$questionId])) {
8061
                            echo Display::return_message(
8062
                                get_lang('You already answered the question'),
8063
                                'normal'
8064
                            );
8065
8066
                            break;
8067
                        }
8068
                    }
8069
                }
8070
            }
8071
8072
            // The $questionList contains the media id we check
8073
            // if this questionId is a media question type
8074
            if (isset($mediaQuestions[$questionId]) &&
8075
                999 != $mediaQuestions[$questionId]
8076
            ) {
8077
                // The question belongs to a media
8078
                $mediaQuestionList = $mediaQuestions[$questionId];
8079
                $objQuestionTmp = Question::read($questionId);
8080
8081
                $counter = 1;
8082
                if (MEDIA_QUESTION == $objQuestionTmp->type) {
8083
                    echo $objQuestionTmp->show_media_content();
8084
8085
                    $countQuestionsInsideMedia = count($mediaQuestionList);
8086
8087
                    // Show questions that belongs to a media
8088
                    if (!empty($mediaQuestionList)) {
8089
                        // In order to parse media questions we use letters a, b, c, etc.
8090
                        $letterCounter = 97;
8091
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
8092
                            $isLastQuestionInMedia = false;
8093
                            if ($counter == $countQuestionsInsideMedia) {
8094
                                $isLastQuestionInMedia = true;
8095
                            }
8096
                            $this->renderQuestion(
8097
                                $questionIdInsideMedia,
8098
                                $attemptList,
8099
                                $remindList,
8100
                                chr($letterCounter),
8101
                                $currentQuestion,
8102
                                $mediaQuestionList,
8103
                                $isLastQuestionInMedia,
8104
                                $questionList
8105
                            );
8106
                            $letterCounter++;
8107
                            $counter++;
8108
                        }
8109
                    }
8110
                } else {
8111
                    $this->renderQuestion(
8112
                        $questionId,
8113
                        $attemptList,
8114
                        $remindList,
8115
                        $i,
8116
                        $currentQuestion,
8117
                        null,
8118
                        null,
8119
                        $questionList
8120
                    );
8121
                    $i++;
8122
                }
8123
            } else {
8124
                // Normal question render.
8125
                $this->renderQuestion(
8126
                    $questionId,
8127
                    $attemptList,
8128
                    $remindList,
8129
                    $i,
8130
                    $currentQuestion,
8131
                    null,
8132
                    null,
8133
                    $questionList
8134
                );
8135
            }
8136
8137
            // For sequential exercises.
8138
            if (ONE_PER_PAGE == $this->type) {
8139
                // quits the loop
8140
                break;
8141
            }
8142
        }
8143
        // end foreach()
8144
8145
        if (ALL_ON_ONE_PAGE == $this->type) {
8146
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
8147
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
8148
        }
8149
    }
8150
8151
    /**
8152
     * Not implemented in 1.11.x.
8153
     *
8154
     * @param int   $questionId
8155
     * @param array $attemptList
8156
     * @param array $remindList
8157
     * @param int   $i
8158
     * @param int   $current_question
8159
     * @param array $questions_in_media
8160
     * @param bool  $last_question_in_media
8161
     * @param array $realQuestionList
8162
     * @param bool  $generateJS
8163
     */
8164
    public function renderQuestion(
8165
        $questionId,
8166
        $attemptList,
8167
        $remindList,
8168
        $i,
8169
        $current_question,
8170
        $questions_in_media = [],
8171
        $last_question_in_media = false,
8172
        $realQuestionList = [],
8173
        $generateJS = true
8174
    ) {
8175
        // With this option on the question is loaded via AJAX
8176
        //$generateJS = true;
8177
        //$this->loadQuestionAJAX = true;
8178
8179
        if ($generateJS && $this->loadQuestionAJAX) {
8180
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
8181
            $params = [
8182
                'questionId' => $questionId,
8183
                'attemptList' => $attemptList,
8184
                'remindList' => $remindList,
8185
                'i' => $i,
8186
                'current_question' => $current_question,
8187
                'questions_in_media' => $questions_in_media,
8188
                'last_question_in_media' => $last_question_in_media,
8189
            ];
8190
            $params = json_encode($params);
8191
8192
            $script = '<script>
8193
            $(function(){
8194
                var params = '.$params.';
8195
                $.ajax({
8196
                    type: "GET",
8197
                    data: params,
8198
                    url: "'.$url.'",
8199
                    success: function(return_value) {
8200
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
8201
                    }
8202
                });
8203
            });
8204
            </script>
8205
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
8206
            echo $script;
8207
        } else {
8208
            $origin = api_get_origin();
8209
            $question_obj = Question::read($questionId);
8210
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
8211
            $remind_highlight = null;
8212
8213
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
8214
            // see #4542 no_remind_highlight class hide with jquery
8215
            if (ALL_ON_ONE_PAGE == $this->type && isset($_GET['reminder']) && 2 == $_GET['reminder']) {
8216
                $remind_highlight = 'no_remind_highlight';
8217
                // @todo not implemented in 1.11.x
8218
                /*if (in_array($question_obj->type, Question::question_type_no_review())) {
8219
                    return null;
8220
                }*/
8221
            }
8222
8223
            $attributes = ['id' => 'remind_list['.$questionId.']'];
8224
8225
            // Showing the question
8226
            $exercise_actions = null;
8227
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
8228
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
8229
8230
            // Shows the question + possible answers
8231
            $showTitle = 1 == $this->getHideQuestionTitle() ? false : true;
8232
            // @todo not implemented in 1.11.x
8233
            /*echo $this->showQuestion(
8234
                $question_obj,
8235
                false,
8236
                $origin,
8237
                $i,
8238
                $showTitle,
8239
                false,
8240
                $user_choice,
8241
                false,
8242
                null,
8243
                false,
8244
                $this->getModelType(),
8245
                $this->categoryMinusOne
8246
            );*/
8247
8248
            // Button save and continue
8249
            switch ($this->type) {
8250
                case ONE_PER_PAGE:
8251
                    $exercise_actions .= $this->show_button(
8252
                        $questionId,
8253
                        $current_question,
8254
                        null,
8255
                        $remindList
8256
                    );
8257
8258
                    break;
8259
                case ALL_ON_ONE_PAGE:
8260
                    if (api_is_allowed_to_session_edit()) {
8261
                        $button = [
8262
                            Display::button(
8263
                                'save_now',
8264
                                get_lang('Save and continue'),
8265
                                ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
8266
                            ),
8267
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
8268
                        ];
8269
                        $exercise_actions .= Display::div(
8270
                            implode(PHP_EOL, $button),
8271
                            ['class' => 'exercise_save_now_button mb-4']
8272
                        );
8273
                    }
8274
8275
                    break;
8276
            }
8277
8278
            if (!empty($questions_in_media)) {
8279
                $count_of_questions_inside_media = count($questions_in_media);
8280
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
8281
                    $button = [
8282
                        Display::button(
8283
                            'save_now',
8284
                            get_lang('Save and continue'),
8285
                            ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
8286
                        ),
8287
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
8288
                    ];
8289
                    $exercise_actions = Display::div(
8290
                        implode(PHP_EOL, $button),
8291
                        ['class' => 'exercise_save_now_button mb-4']
8292
                    );
8293
                }
8294
8295
                if ($last_question_in_media && ONE_PER_PAGE == $this->type) {
8296
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
8297
                }
8298
            }
8299
8300
            // Checkbox review answers. Not implemented.
8301
            /*if ($this->review_answers &&
8302
                !in_array($question_obj->type, Question::question_type_no_review())
8303
            ) {
8304
                $remind_question_div = Display::tag(
8305
                    'label',
8306
                    Display::input(
8307
                        'checkbox',
8308
                        'remind_list['.$questionId.']',
8309
                        '',
8310
                        $attributes
8311
                    ).get_lang('Revise question later'),
8312
                    [
8313
                        'class' => 'checkbox',
8314
                        'for' => 'remind_list['.$questionId.']',
8315
                    ]
8316
                );
8317
                $exercise_actions .= Display::div(
8318
                    $remind_question_div,
8319
                    ['class' => 'exercise_save_now_button']
8320
                );
8321
            }*/
8322
8323
            echo Display::div(' ', ['class' => 'clear']);
8324
8325
            $paginationCounter = null;
8326
            if (ONE_PER_PAGE == $this->type) {
8327
                if (empty($questions_in_media)) {
8328
                    $paginationCounter = Display::paginationIndicator(
8329
                        $current_question,
8330
                        count($realQuestionList)
8331
                    );
8332
                } else {
8333
                    if ($last_question_in_media) {
8334
                        $paginationCounter = Display::paginationIndicator(
8335
                            $current_question,
8336
                            count($realQuestionList)
8337
                        );
8338
                    }
8339
                }
8340
            }
8341
8342
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
8343
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
8344
            echo '</div>';
8345
        }
8346
    }
8347
8348
    /**
8349
     * Returns an array of categories details for the questions of the current
8350
     * exercise.
8351
     *
8352
     * @return array
8353
     */
8354
    public function getQuestionWithCategories()
8355
    {
8356
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
8357
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
8358
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8359
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
8360
        $sql = "SELECT DISTINCT cat.*
8361
                FROM $TBL_EXERCICE_QUESTION e
8362
                INNER JOIN $TBL_QUESTIONS q
8363
                ON (e.question_id = q.iid)
8364
                INNER JOIN $categoryRelTable catRel
8365
                ON (catRel.question_id = e.question_id)
8366
                INNER JOIN $categoryTable cat
8367
                ON (cat.iid = catRel.category_id)
8368
                WHERE
8369
                  e.quiz_id	= ".(int) ($this->getId());
8370
8371
        $result = Database::query($sql);
8372
        $categoriesInExercise = [];
8373
        if (Database::num_rows($result)) {
8374
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
8375
        }
8376
8377
        return $categoriesInExercise;
8378
    }
8379
8380
    /**
8381
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
8382
     */
8383
    public function getMaxScore()
8384
    {
8385
        $outMaxScore = 0;
8386
        // list of question's id !!! the array key start at 1 !!!
8387
        $questionList = $this->selectQuestionList(true);
8388
8389
        if ($this->random > 0 && $this->randomByCat > 0) {
8390
            // test is random by category
8391
            // get the $numberRandomQuestions best score question of each category
8392
            $numberRandomQuestions = $this->random;
8393
            $tabCategoriesScores = [];
8394
            foreach ($questionList as $questionId) {
8395
                $questionCategoryId = TestCategory::getCategoryForQuestion($questionId);
8396
                if (!is_array($tabCategoriesScores[$questionCategoryId])) {
8397
                    $tabCategoriesScores[$questionCategoryId] = [];
8398
                }
8399
                $tmpObjQuestion = Question::read($questionId);
8400
                if (is_object($tmpObjQuestion)) {
8401
                    $tabCategoriesScores[$questionCategoryId][] = $tmpObjQuestion->weighting;
8402
                }
8403
            }
8404
8405
            // here we've got an array with first key, the category_id, second key, score of question for this cat
8406
            foreach ($tabCategoriesScores as $tabScores) {
8407
                rsort($tabScores);
8408
                $tabScoresCount = count($tabScores);
8409
                for ($i = 0; $i < min($numberRandomQuestions, $tabScoresCount); $i++) {
8410
                    $outMaxScore += $tabScores[$i];
8411
                }
8412
            }
8413
8414
            return $outMaxScore;
8415
        }
8416
8417
        // standard test, just add each question score
8418
        foreach ($questionList as $questionId) {
8419
            $question = Question::read($questionId, $this->course);
8420
            $outMaxScore += $question->weighting;
8421
        }
8422
8423
        return $outMaxScore;
8424
    }
8425
8426
    /**
8427
     * @return string
8428
     */
8429
    public function get_formated_title()
8430
    {
8431
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
8432
        }
8433
8434
        return api_html_entity_decode($this->selectTitle());
8435
    }
8436
8437
    /**
8438
     * @param string $title
8439
     *
8440
     * @return string
8441
     */
8442
    public static function get_formated_title_variable($title)
8443
    {
8444
        return api_html_entity_decode($title);
8445
    }
8446
8447
    /**
8448
     * @return string
8449
     */
8450
    public function format_title()
8451
    {
8452
        return api_htmlentities($this->title);
8453
    }
8454
8455
    /**
8456
     * @param string $title
8457
     *
8458
     * @return string
8459
     */
8460
    public static function format_title_variable($title)
8461
    {
8462
        return api_htmlentities($title);
8463
    }
8464
8465
    /**
8466
     * @param int $courseId
8467
     * @param int $sessionId
8468
     *
8469
     * @return array exercises
8470
     */
8471
    public function getExercisesByCourseSession($courseId, $sessionId)
8472
    {
8473
        $courseId = (int) $courseId;
8474
        $sessionId = (int) $sessionId;
8475
8476
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
8477
        $sql = "SELECT * FROM $tbl_quiz cq
8478
                WHERE
8479
                    cq.c_id = %s AND
8480
                    (cq.session_id = %s OR cq.session_id = 0)
8481
                ORDER BY cq.iid";
8482
        $sql = sprintf($sql, $courseId, $sessionId);
8483
8484
        $result = Database::query($sql);
8485
8486
        $rows = [];
8487
        while ($row = Database::fetch_assoc($result)) {
8488
            $rows[] = $row;
8489
        }
8490
8491
        return $rows;
8492
    }
8493
8494
    /**
8495
     * @param int   $courseId
8496
     * @param int   $sessionId
8497
     * @param array $quizId
8498
     *
8499
     * @return array exercises
8500
     */
8501
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
8502
    {
8503
        if (empty($quizId)) {
8504
            return [];
8505
        }
8506
8507
        $sessionId = (int) $sessionId;
8508
        $courseId = (int) $courseId;
8509
8510
        $ids = is_array($quizId) ? $quizId : [$quizId];
8511
        $ids = array_map('intval', $ids);
8512
        $ids = implode(',', $ids);
8513
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8514
        if (0 != $sessionId) {
8515
            $sql = "SELECT * FROM $track_exercises te
8516
              INNER JOIN c_quiz cq
8517
              ON cq.iid = te.exe_exo_id
8518
              WHERE
8519
              te.c_id = %d AND
8520
              te.session_id = %s AND
8521
              cq.iid IN (%s)
8522
              ORDER BY cq.iid";
8523
8524
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
8525
        } else {
8526
            $sql = "SELECT * FROM $track_exercises te
8527
              INNER JOIN c_quiz cq ON cq.iid = te.exe_exo_id
8528
              WHERE
8529
              te.c_id = %d AND
8530
              cq.iid IN (%s)
8531
              ORDER BY cq.iid";
8532
            $sql = sprintf($sql, $courseId, $ids);
8533
        }
8534
        $result = Database::query($sql);
8535
        $rows = [];
8536
        while ($row = Database::fetch_assoc($result)) {
8537
            $rows[] = $row;
8538
        }
8539
8540
        return $rows;
8541
    }
8542
8543
    /**
8544
     * @param $exeId
8545
     * @param $exercise_stat_info
8546
     * @param $remindList
8547
     * @param $currentQuestion
8548
     *
8549
     * @return int|null
8550
     */
8551
    public static function getNextQuestionId(
8552
        $exeId,
8553
        $exercise_stat_info,
8554
        $remindList,
8555
        $currentQuestion
8556
    ) {
8557
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8558
8559
        if (isset($result[$exeId])) {
8560
            $result = $result[$exeId];
8561
        } else {
8562
            return null;
8563
        }
8564
8565
        $data_tracking = $exercise_stat_info['data_tracking'];
8566
        $data_tracking = explode(',', $data_tracking);
8567
8568
        // if this is the final question do nothing.
8569
        if ($currentQuestion == count($data_tracking)) {
8570
            return null;
8571
        }
8572
8573
        $currentQuestion--;
8574
8575
        if (!empty($result['question_list'])) {
8576
            $answeredQuestions = [];
8577
            foreach ($result['question_list'] as $question) {
8578
                if (!empty($question['answer'])) {
8579
                    $answeredQuestions[] = $question['question_id'];
8580
                }
8581
            }
8582
8583
            // Checking answered questions
8584
            $counterAnsweredQuestions = 0;
8585
            foreach ($data_tracking as $questionId) {
8586
                if (!in_array($questionId, $answeredQuestions)) {
8587
                    if ($currentQuestion != $counterAnsweredQuestions) {
8588
                        break;
8589
                    }
8590
                }
8591
                $counterAnsweredQuestions++;
8592
            }
8593
8594
            $counterRemindListQuestions = 0;
8595
            // Checking questions saved in the reminder list
8596
            if (!empty($remindList)) {
8597
                foreach ($data_tracking as $questionId) {
8598
                    if (in_array($questionId, $remindList)) {
8599
                        // Skip the current question
8600
                        if ($currentQuestion != $counterRemindListQuestions) {
8601
                            break;
8602
                        }
8603
                    }
8604
                    $counterRemindListQuestions++;
8605
                }
8606
8607
                if ($counterRemindListQuestions < $currentQuestion) {
8608
                    return null;
8609
                }
8610
8611
                if (!empty($counterRemindListQuestions)) {
8612
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8613
                        return $counterAnsweredQuestions;
8614
                    } else {
8615
                        return $counterRemindListQuestions;
8616
                    }
8617
                }
8618
            }
8619
8620
            return $counterAnsweredQuestions;
8621
        }
8622
    }
8623
8624
    /**
8625
     * Gets the position of a questionId in the question list.
8626
     *
8627
     * @param $questionId
8628
     *
8629
     * @return int
8630
     */
8631
    public function getPositionInCompressedQuestionList($questionId)
8632
    {
8633
        $questionList = $this->getQuestionListWithMediasCompressed();
8634
        $mediaQuestions = $this->getMediaList();
8635
        $position = 1;
8636
        foreach ($questionList as $id) {
8637
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8638
                $mediaQuestionList = $mediaQuestions[$id];
8639
                if (in_array($questionId, $mediaQuestionList)) {
8640
                    return $position;
8641
                } else {
8642
                    $position++;
8643
                }
8644
            } else {
8645
                if ($id == $questionId) {
8646
                    return $position;
8647
                } else {
8648
                    $position++;
8649
                }
8650
            }
8651
        }
8652
8653
        return 1;
8654
    }
8655
8656
    /**
8657
     * Get the correct answers in all attempts.
8658
     *
8659
     * @param int  $learnPathId
8660
     * @param int  $learnPathItemId
8661
     * @param bool $onlyCorrect
8662
     *
8663
     * @return array
8664
     */
8665
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8666
    {
8667
        $attempts = Event::getExerciseResultsByUser(
8668
            api_get_user_id(),
8669
            $this->getId(),
8670
            api_get_course_int_id(),
8671
            api_get_session_id(),
8672
            $learnPathId,
8673
            $learnPathItemId,
8674
            'DESC'
8675
        );
8676
8677
        $list = [];
8678
        foreach ($attempts as $attempt) {
8679
            foreach ($attempt['question_list'] as $answers) {
8680
                foreach ($answers as $answer) {
8681
                    $objAnswer = new Answer($answer['question_id']);
8682
                    if ($onlyCorrect) {
8683
                        switch ($objAnswer->getQuestionType()) {
8684
                            case FILL_IN_BLANKS:
8685
                            case FILL_IN_BLANKS_COMBINATION:
8686
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8687
8688
                                break;
8689
                            case MATCHING:
8690
                            case MATCHING_COMBINATION:
8691
                            case DRAGGABLE:
8692
                            case MATCHING_DRAGGABLE:
8693
                            case MATCHING_DRAGGABLE_COMBINATION:
8694
                                $isCorrect = Matching::isCorrect(
8695
                                    $answer['position'],
8696
                                    $answer['answer'],
8697
                                    $answer['question_id']
8698
                                );
8699
8700
                                break;
8701
                            case ORAL_EXPRESSION:
8702
                                $isCorrect = false;
8703
8704
                                break;
8705
                            default:
8706
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8707
                        }
8708
                        if ($isCorrect) {
8709
                            $list[$answer['question_id']][] = $answer;
8710
                        }
8711
                    } else {
8712
                        $list[$answer['question_id']][] = $answer;
8713
                    }
8714
                }
8715
            }
8716
8717
            if (false === $onlyCorrect) {
8718
                // Only take latest attempt
8719
                break;
8720
            }
8721
        }
8722
8723
        return $list;
8724
    }
8725
8726
    /**
8727
     * Get the correct answers in all attempts.
8728
     *
8729
     * @param int $learnPathId
8730
     * @param int $learnPathItemId
8731
     *
8732
     * @return array
8733
     */
8734
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8735
    {
8736
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8737
    }
8738
8739
    /**
8740
     * @return bool
8741
     */
8742
    public function showPreviousButton()
8743
    {
8744
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
8745
        if (false === $allow) {
8746
            return true;
8747
        }
8748
8749
        return $this->showPreviousButton;
8750
    }
8751
8752
    public function getPreventBackwards()
8753
    {
8754
        return (int) $this->preventBackwards;
8755
    }
8756
8757
    /**
8758
     * @return int
8759
     */
8760
    public function getQuizCategoryId(): ?int
8761
    {
8762
        if (empty($this->quizCategoryId)) {
8763
            return null;
8764
        }
8765
8766
        return (int) $this->quizCategoryId;
8767
    }
8768
8769
    /**
8770
     * @param int $value
8771
     */
8772
    public function setQuizCategoryId($value): void
8773
    {
8774
        if (!empty($value)) {
8775
            $this->quizCategoryId = (int) $value;
8776
        }
8777
    }
8778
8779
    /**
8780
     * Set the value to 1 to hide the question number.
8781
     *
8782
     * @param int $value
8783
     */
8784
    public function setHideQuestionNumber($value = 0)
8785
    {
8786
        $this->hideQuestionNumber = (int) $value;
8787
    }
8788
8789
    /**
8790
     * Gets the value to hide or show the question number. If it does not exist, it is set to 0.
8791
     *
8792
     * @return int 1 if the question number must be hidden
8793
     */
8794
    public function getHideQuestionNumber()
8795
    {
8796
        return (int) $this->hideQuestionNumber;
8797
    }
8798
8799
    public function setPageResultConfiguration(array $values)
8800
    {
8801
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8802
        if ($pageConfig) {
8803
            $params = [
8804
                'hide_expected_answer' => $values['hide_expected_answer'] ?? '',
8805
                'hide_question_score' => $values['hide_question_score'] ?? '',
8806
                'hide_total_score' => $values['hide_total_score'] ?? '',
8807
                'hide_category_table' => $values['hide_category_table'] ?? '',
8808
                'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
8809
            ];
8810
            $this->pageResultConfiguration = $params;
8811
        }
8812
    }
8813
8814
    /**
8815
     * @param array $defaults
8816
     */
8817
    public function setPageResultConfigurationDefaults(&$defaults)
8818
    {
8819
        $configuration = $this->getPageResultConfiguration();
8820
        if (!empty($configuration) && !empty($defaults)) {
8821
            $defaults = array_merge($defaults, $configuration);
8822
        }
8823
    }
8824
8825
    /**
8826
     * @return array
8827
     */
8828
    public function getPageResultConfiguration()
8829
    {
8830
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8831
        if ($pageConfig) {
8832
            return $this->pageResultConfiguration;
8833
        }
8834
8835
        return [];
8836
    }
8837
8838
    /**
8839
     * @param string $attribute
8840
     *
8841
     * @return mixed|null
8842
     */
8843
    public function getPageConfigurationAttribute($attribute)
8844
    {
8845
        $result = $this->getPageResultConfiguration();
8846
8847
        if (!empty($result)) {
8848
            return $result[$attribute] ?? null;
8849
        }
8850
8851
        return null;
8852
    }
8853
8854
    /**
8855
     * @param bool $showPreviousButton
8856
     *
8857
     * @return Exercise
8858
     */
8859
    public function setShowPreviousButton($showPreviousButton)
8860
    {
8861
        $this->showPreviousButton = $showPreviousButton;
8862
8863
        return $this;
8864
    }
8865
8866
    /**
8867
     * @param array $notifications
8868
     */
8869
    public function setNotifications($notifications)
8870
    {
8871
        $this->notifications = $notifications;
8872
    }
8873
8874
    /**
8875
     * @return array
8876
     */
8877
    public function getNotifications()
8878
    {
8879
        return $this->notifications;
8880
    }
8881
8882
    /**
8883
     * @return bool
8884
     */
8885
    public function showExpectedChoice()
8886
    {
8887
        return ('true' === api_get_setting('exercise.show_exercise_expected_choice'));
8888
    }
8889
8890
    /**
8891
     * @return bool
8892
     */
8893
    public function showExpectedChoiceColumn()
8894
    {
8895
        if (true === $this->forceShowExpectedChoiceColumn) {
8896
            return true;
8897
        }
8898
        if ($this->hideExpectedAnswer) {
8899
            return false;
8900
        }
8901
        if (!in_array(
8902
            $this->results_disabled,
8903
            [
8904
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8905
            ]
8906
        )
8907
        ) {
8908
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8909
            if (1 === $hide) {
8910
                return false;
8911
            }
8912
8913
            return true;
8914
        }
8915
8916
        return false;
8917
    }
8918
8919
    public function getQuestionRibbon(string $class, string $scoreLabel, ?string $result, array $array): string
8920
    {
8921
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8922
        if (1 === $hide) {
8923
            return '';
8924
        }
8925
8926
        $ribbon = '<div class="question-answer-result__header-ribbon-title question-answer-result__header-ribbon-title--'.$class.'">'.$scoreLabel.'</div>';
8927
        if (!empty($result)) {
8928
            $ribbon .= '<div class="question-answer-result__header-ribbon-detail">'
8929
                .get_lang('Score').': '.$result
8930
                .'</div>';
8931
        }
8932
8933
        $ribbonClassModifier = '';
8934
8935
        if ($this->showExpectedChoice()) {
8936
            $hideLabel = ('true' === api_get_setting('exercise.exercise_hide_label'));
8937
            if (true === $hideLabel) {
8938
                $ribbonClassModifier = 'question-answer-result__header-ribbon--no-ribbon';
8939
                $html = '';
8940
                $answerUsed = (int) $array['used'];
8941
                $answerMissing = (int) $array['missing'] - $answerUsed;
8942
                for ($i = 1; $i <= $answerUsed; $i++) {
8943
                    $html .= Display::getMdiIcon(StateIcon::COMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8944
                }
8945
                for ($i = 1; $i <= $answerMissing; $i++) {
8946
                    $html .= Display::getMdiIcon(StateIcon::INCOMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8947
                }
8948
                $ribbon = '<div class="question-answer-result__header-ribbon-title hide-label-title">'
8949
                    .get_lang('Correct answers').': '.$result.'</div>'
8950
                    .'<div class="question-answer-result__header-ribbon-detail">'.$html.'</div>';
8951
            }
8952
        }
8953
8954
        return Display::div(
8955
            $ribbon,
8956
            ['class' => "question-answer-result__header-ribbon $ribbonClassModifier"]
8957
        );
8958
    }
8959
8960
    /**
8961
     * @return int
8962
     */
8963
    public function getAutoLaunch()
8964
    {
8965
        return $this->autolaunch;
8966
    }
8967
8968
    /**
8969
     * Clean auto launch settings for all exercise in course/course-session.
8970
     */
8971
    public function enableAutoLaunch()
8972
    {
8973
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8974
        $sql = "UPDATE $table SET autolaunch = 1
8975
                WHERE iid = ".$this->iId;
8976
        Database::query($sql);
8977
    }
8978
8979
    /**
8980
     * Clean auto launch settings for all exercise in course/course-session.
8981
     */
8982
    public function cleanCourseLaunchSettings()
8983
    {
8984
        $em = Database::getManager();
8985
8986
        $repo = Container::getQuizRepository();
8987
8988
        $session = api_get_session_entity();
8989
        $course = api_get_course_entity();
8990
8991
        $qb = $repo->getResourcesByCourse($course, $session);
8992
        $quizzes = $qb->getQuery()->getResult();
8993
8994
        foreach ($quizzes as $quiz) {
8995
            $quiz->setAutoLaunch(false);
8996
            $em->persist($quiz);
8997
        }
8998
8999
        $em->flush();
9000
    }
9001
9002
    /**
9003
     * Get the title without HTML tags.
9004
     *
9005
     * @return string
9006
     */
9007
    public function getUnformattedTitle()
9008
    {
9009
        return strip_tags(api_html_entity_decode($this->title));
9010
    }
9011
9012
    /**
9013
     * Get the question IDs from quiz_rel_question for the current quiz,
9014
     * using the parameters as the arguments to the SQL's LIMIT clause.
9015
     * Because the exercise_id is known, it also comes with a filter on
9016
     * the session, so sessions are not specified here.
9017
     *
9018
     * @param int $start  At which question do we want to start the list
9019
     * @param int $length Up to how many results we want
9020
     *
9021
     * @return array A list of question IDs
9022
     */
9023
    public function getQuestionForTeacher($start = 0, $length = 10)
9024
    {
9025
        $start = (int) $start;
9026
        if ($start < 0) {
9027
            $start = 0;
9028
        }
9029
9030
        $length = (int) $length;
9031
9032
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
9033
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
9034
        $sql = "SELECT DISTINCT e.question_id
9035
                FROM $quizRelQuestion e
9036
                INNER JOIN $question q
9037
                ON (e.question_id = q.iid)
9038
                WHERE
9039
9040
                    e.quiz_id = '".$this->getId()."'
9041
                ORDER BY question_order
9042
                LIMIT $start, $length
9043
            ";
9044
        $result = Database::query($sql);
9045
        $questionList = [];
9046
        while ($object = Database::fetch_object($result)) {
9047
            $questionList[] = $object->question_id;
9048
        }
9049
9050
        return $questionList;
9051
    }
9052
9053
    /**
9054
     * @param int   $exerciseId
9055
     * @param array $courseInfo
9056
     * @param int   $sessionId
9057
     *
9058
     * @return bool
9059
     */
9060
    public function generateStats($exerciseId, $courseInfo, $sessionId)
9061
    {
9062
        $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
9063
        if (!$allowStats) {
9064
            return false;
9065
        }
9066
9067
        if (empty($courseInfo)) {
9068
            return false;
9069
        }
9070
9071
        $courseId = $courseInfo['real_id'];
9072
9073
        $sessionId = (int) $sessionId;
9074
        $exerciseId = (int) $exerciseId;
9075
9076
        $result = $this->read($exerciseId);
9077
9078
        if (empty($result)) {
9079
            api_not_allowed(true);
9080
        }
9081
9082
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
9083
9084
        $studentList = CourseManager::get_user_list_from_course_code(
9085
            $courseInfo['code'],
9086
            $sessionId,
9087
            null,
9088
            null,
9089
            $statusToFilter
9090
        );
9091
9092
        if (empty($studentList)) {
9093
            Display::addFlash(Display::return_message(get_lang('No users in course')));
9094
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
9095
            exit;
9096
        }
9097
9098
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9099
9100
        $studentIdList = [];
9101
        if (!empty($studentList)) {
9102
            $studentIdList = array_column($studentList, 'user_id');
9103
        }
9104
9105
        $sessionCondition = api_get_session_condition($sessionId);
9106
        if (false == $this->exercise_was_added_in_lp) {
9107
            $sql = "SELECT * FROM $tblStats
9108
                        WHERE
9109
                            exe_exo_id = $exerciseId AND
9110
                            orig_lp_id = 0 AND
9111
                            orig_lp_item_id = 0 AND
9112
                            status <> 'incomplete' AND
9113
                            c_id = $courseId
9114
                            $sessionCondition
9115
                        ";
9116
        } else {
9117
            $lpId = null;
9118
            if (!empty($this->lpList)) {
9119
                // Taking only the first LP
9120
                $lpId = $this->getLpBySession($sessionId);
9121
                $lpId = $lpId['lp_id'];
9122
            }
9123
9124
            $sql = "SELECT *
9125
                        FROM $tblStats
9126
                        WHERE
9127
                            exe_exo_id = $exerciseId AND
9128
                            orig_lp_id = $lpId AND
9129
                            status <> 'incomplete' AND
9130
                            session_id = $sessionId AND
9131
                            c_id = $courseId ";
9132
        }
9133
9134
        $sql .= ' ORDER BY exe_id DESC';
9135
9136
        $studentCount = 0;
9137
        $sum = 0;
9138
        $bestResult = 0;
9139
        $sumResult = 0;
9140
        $result = Database::query($sql);
9141
        while ($data = Database::fetch_assoc($result)) {
9142
            // Only take into account users in the current student list.
9143
            if (!empty($studentIdList)) {
9144
                if (!in_array($data['exe_user_id'], $studentIdList)) {
9145
                    continue;
9146
                }
9147
            }
9148
9149
            if (!isset($students[$data['exe_user_id']])) {
9150
                if (0 != $data['max_score']) {
9151
                    $students[$data['exe_user_id']] = $data['score'];
9152
                    if ($data['score'] > $bestResult) {
9153
                        $bestResult = $data['score'];
9154
                    }
9155
                    $sumResult += $data['score'];
9156
                }
9157
            }
9158
        }
9159
9160
        $count = count($studentList);
9161
        $average = $sumResult / $count;
9162
        $em = Database::getManager();
9163
9164
        $links = AbstractLink::getGradebookLinksFromItem(
9165
            $this->getId(),
9166
            LINK_EXERCISE,
9167
            $courseInfo['real_id'],
9168
            $sessionId
9169
        );
9170
9171
        if (empty($links)) {
9172
            $links = AbstractLink::getGradebookLinksFromItem(
9173
                $this->iId,
9174
                LINK_EXERCISE,
9175
                $courseInfo['real_id'],
9176
                $sessionId
9177
            );
9178
        }
9179
9180
        if (!empty($links)) {
9181
            $repo = $em->getRepository(GradebookLink::class);
9182
9183
            foreach ($links as $link) {
9184
                $linkId = $link['id'];
9185
                /** @var GradebookLink $exerciseLink */
9186
                $exerciseLink = $repo->find($linkId);
9187
                if ($exerciseLink) {
9188
                    $exerciseLink
9189
                        ->setUserScoreList($students)
9190
                        ->setBestScore($bestResult)
9191
                        ->setAverageScore($average)
9192
                        ->setScoreWeight($this->getMaxScore());
9193
                    $em->persist($exerciseLink);
9194
                    $em->flush();
9195
                }
9196
            }
9197
        }
9198
    }
9199
9200
    /**
9201
     * Return an HTML table of exercises for on-screen printing, including
9202
     * action icons. If no exercise is present and the user can edit the
9203
     * course, show a "create test" button.
9204
     *
9205
     * @param int    $categoryId
9206
     * @param string $keyword
9207
     * @param int    $userId
9208
     * @param int    $courseId
9209
     * @param int    $sessionId
9210
     * @param bool   $returnData
9211
     * @param int    $minCategoriesInExercise
9212
     * @param int    $filterByResultDisabled
9213
     * @param int    $filterByAttempt
9214
     *
9215
     * @return string|SortableTableFromArrayConfig
9216
     */
9217
    public static function exerciseGridResource(
9218
        $categoryId,
9219
        $keyword = '',
9220
        $userId = 0,
9221
        $courseId = 0,
9222
        $sessionId = 0,
9223
        $returnData = false,
9224
        $minCategoriesInExercise = 0,
9225
        $filterByResultDisabled = 0,
9226
        $filterByAttempt = 0,
9227
        $myActions = null,
9228
        $returnTable = false
9229
    ) {
9230
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
9231
        $courseId = $courseId ? (int) $courseId : api_get_course_int_id();
9232
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
9233
9234
        $course = api_get_course_entity($courseId);
9235
        $session = api_get_session_entity($sessionId);
9236
9237
        $userId = $userId ? (int) $userId : api_get_user_id();
9238
        $user = api_get_user_entity($userId);
9239
9240
        $repo = Container::getQuizRepository();
9241
9242
        $trackEExerciseRepo = Container::getTrackEExerciseRepository();
9243
        $pendingCorrections = $trackEExerciseRepo->getPendingCorrectionsByExercise($courseId);
9244
        $pendingAttempts = [];
9245
        foreach ($pendingCorrections as $correction) {
9246
            $pendingAttempts[$correction['exerciseId']] = $correction['pendingCount'];
9247
        }
9248
9249
        // 2. Get query builder from repo.
9250
        $qb = $repo->getResourcesByCourse($course, $session);
9251
9252
        if (!empty($sessionId)) {
9253
            $qb->andWhere('(links.session = :sess OR links.session IS NULL)')
9254
                ->setParameter('sess', $session);
9255
        } else {
9256
            $qb->andWhere('links.session IS NULL');
9257
        }
9258
9259
        if (!empty($categoryId)) {
9260
            $qb->andWhere($qb->expr()->eq('resource.quizCategory', $categoryId));
9261
        } else {
9262
            $qb->andWhere($qb->expr()->isNull('resource.quizCategory'));
9263
        }
9264
9265
        $allowDelete = self::allowAction('delete');
9266
        $allowClean = self::allowAction('clean_results');
9267
9268
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9269
9270
        $categoryId = (int) $categoryId;
9271
        $keyword = Database::escape_string($keyword);
9272
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
9273
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
9274
9275
        $courseId = $course->getId();
9276
        $tableRows = [];
9277
        $origin = api_get_origin();
9278
        $charset = 'utf-8';
9279
        $token = Security::get_token();
9280
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, ['real_id' => $courseId]);
9281
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
9282
        $content = '';
9283
        $column = 0;
9284
        if ($is_allowedToEdit) {
9285
            $column = 1;
9286
        }
9287
9288
        $table = new SortableTableFromArrayConfig(
9289
            [],
9290
            $column,
9291
            self::PAGINATION_ITEMS_PER_PAGE,
9292
            'exercises_cat_'.$categoryId.'_'.api_get_course_int_id().'_'.api_get_session_id()
9293
        );
9294
9295
        $limit = $table->per_page;
9296
        $page = $table->page_nr;
9297
        $from = $limit * ($page - 1);
9298
9299
        $categoryCondition = '';
9300
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
9301
            if (!empty($categoryId)) {
9302
                $categoryCondition = " AND quiz_category_id = $categoryId ";
9303
            } else {
9304
                $categoryCondition = ' AND quiz_category_id IS NULL ';
9305
            }
9306
        }
9307
9308
        if (!empty($keyword)) {
9309
            $qb->andWhere($qb->expr()->like('resource.title', ':keyword'));
9310
            $qb->setParameter('keyword', '%'.$keyword.'%');
9311
        }
9312
9313
        if (!$is_allowedToEdit) {
9314
            $qb->andWhere('links.visibility = :published')
9315
                ->setParameter('published', ResourceLink::VISIBILITY_PUBLISHED);
9316
        }
9317
9318
        $qb->setFirstResult($from);
9319
        $qb->setMaxResults($limit);
9320
9321
        $filterByResultDisabledCondition = '';
9322
        $filterByResultDisabled = (int) $filterByResultDisabled;
9323
        if (!empty($filterByResultDisabled)) {
9324
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
9325
        }
9326
        $filterByAttemptCondition = '';
9327
        $filterByAttempt = (int) $filterByAttempt;
9328
        if (!empty($filterByAttempt)) {
9329
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
9330
        }
9331
9332
        $exerciseList = $qb->getQuery()->getResult();
9333
9334
        $total = $repo->getCount($qb);
9335
9336
        $webPath = api_get_path(WEB_CODE_PATH);
9337
        if (!empty($exerciseList)) {
9338
            $visibilitySetting = ('true' === api_get_setting('lp.show_hidden_exercise_added_to_lp'));
9339
            //avoid sending empty parameters
9340
            $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
9341
            $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
9342
9343
            /** @var CQuiz $exerciseEntity */
9344
            foreach ($exerciseList as $exerciseEntity) {
9345
                $currentRow = [];
9346
                $exerciseId = $exerciseEntity->getIid();
9347
                $actions = '';
9348
                $attempt_text = '';
9349
                $exercise = new Exercise($courseId);
9350
                $exercise->read($exerciseId, false);
9351
9352
                if (empty($exercise->iId)) {
9353
                    continue;
9354
                }
9355
9356
                $sessionId = api_get_session_id();
9357
                $allowToEditBaseCourse = true;
9358
                $visibleBase = $exerciseEntity->isVisible($course);
9359
                $visibleSess = !empty($sessionId) ? $exerciseEntity->isVisible($course, $session) : null;
9360
                if ($visibleSess === true) {
9361
                    $visibleForStudent = true;
9362
                } elseif ($visibleSess === false) {
9363
                    $visibleForStudent = false;
9364
                } else {
9365
                    $visibleForStudent = $visibleBase;
9366
                }
9367
9368
                $visibleForTeacher = true;
9369
9370
                if (!$is_allowedToEdit && !empty($sessionId)) {
9371
                    $visibilitySetting = ('true' === api_get_setting('lp.show_hidden_exercise_added_to_lp'));
9372
                    if (!$visibleBase && !$visibilitySetting && $exercise->exercise_was_added_in_lp) {
9373
                        continue;
9374
                    }
9375
                }
9376
9377
                $isBaseCourseExercise = $visibleBase && ($visibleSess === null || $visibleSess === true);
9378
                if (!empty($sessionId) && $isBaseCourseExercise) {
9379
                    $allowToEditBaseCourse = false;
9380
                }
9381
9382
                $resourceLink = $exerciseEntity->getFirstResourceLink();
9383
                if ($resourceLink && !$sessionId && $resourceLink->getSession() === null) {
9384
                    $allowToEditBaseCourse = true;
9385
                }
9386
9387
                $allowToEditSession = ($resourceLink && $resourceLink->getSession() && $resourceLink->getSession()->getId() === $sessionId);
9388
                $sessionStar = null;
9389
                if ($allowToEditSession) {
9390
                    $sessionStar = api_get_session_image($sessionId, $user);
9391
                }
9392
9393
                $locked = $exercise->is_gradebook_locked;
9394
9395
                $startTime = $exerciseEntity->getStartTime();
9396
                $endTime = $exerciseEntity->getEndTime();
9397
                $time_limits = false;
9398
                if (!empty($startTime) || !empty($endTime)) {
9399
                    $time_limits = true;
9400
                }
9401
9402
                $is_actived_time = false;
9403
                if ($time_limits) {
9404
                    // check if start time
9405
                    $start_time = false;
9406
                    if (!empty($startTime)) {
9407
                        $start_time = api_strtotime($startTime->format('Y-m-d H:i:s'), 'UTC');
9408
                    }
9409
                    $end_time = false;
9410
                    if (!empty($endTime)) {
9411
                        $end_time = api_strtotime($endTime->format('Y-m-d H:i:s'), 'UTC');
9412
                    }
9413
                    $now = time();
9414
                    //If both "clocks" are enable
9415
                    if ($start_time && $end_time) {
9416
                        if ($now > $start_time && $end_time > $now) {
9417
                            $is_actived_time = true;
9418
                        }
9419
                    } else {
9420
                        //we check the start and end
9421
                        if ($start_time) {
9422
                            if ($now > $start_time) {
9423
                                $is_actived_time = true;
9424
                            }
9425
                        }
9426
                        if ($end_time) {
9427
                            if ($end_time > $now) {
9428
                                $is_actived_time = true;
9429
                            }
9430
                        }
9431
                    }
9432
                }
9433
9434
                $cut_title = $exercise->getCutTitle();
9435
                $alt_title = '';
9436
                if ($cut_title != $exerciseEntity->getTitle()) {
9437
                    $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
9438
                }
9439
9440
                // Teacher only.
9441
                if ($is_allowedToEdit) {
9442
                    $lp_blocked = null;
9443
                    if (true == $exercise->exercise_was_added_in_lp) {
9444
                        $lp_blocked = Display::div(
9445
                            get_lang(
9446
                                'This exercise has been included in a learning path, so it cannot be accessed by students directly from here. If you want to put the same exercise available through the exercises tool, please make a copy of the current exercise using the copy icon.'
9447
                            ),
9448
                            ['class' => 'lp_content_type_label']
9449
                        );
9450
                    }
9451
9452
                    $style = $visibleForStudent ? '' : 'color:grey';
9453
9454
                    $title = $cut_title;
9455
9456
                    $url = '<a
9457
                        '.$alt_title.'
9458
                        id="tooltip_'.$exerciseId.'"
9459
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'"
9460
                        style = "'.$style.';float:left;"
9461
                        >
9462
                         '.Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, ICON_SIZE_SMALL, $title).$title.
9463
                        '</a>'.$sessionStar;
9464
9465
                    if (ExerciseLib::isQuizEmbeddable($exerciseEntity)) {
9466
                        $embeddableIcon = Display::getMdiIcon('book-music-outline', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('This quiz can be embeddable on videos or mobile content'));
9467
                        $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9468
                    }
9469
9470
                    $pendingCount = $pendingAttempts[$exerciseId] ?? 0;
9471
                    if ($pendingCount > 0) {
9472
                        $pendingIcon = Display::getMdiIcon(
9473
                            ActionIcon::ALERT->value,
9474
                            'ch-tool-icon',
9475
                            null,
9476
                            ICON_SIZE_SMALL,
9477
                            get_lang('Pending attempts') . ": $pendingCount"
9478
                        );
9479
                        $url .= " $pendingIcon";
9480
                    }
9481
9482
                    $currentRow['title'] = $url.$lp_blocked;
9483
                    $rowi = $exerciseEntity->getQuestions()->count();
9484
                    if ($allowToEditBaseCourse || $allowToEditSession) {
9485
                        // Questions list
9486
                        $actions = Display::url(
9487
                            Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')),
9488
                            'admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9489
                        );
9490
9491
                        // Test settings
9492
                        $settings = Display::url(
9493
                            Display::getMdiIcon(ToolIcon::SETTINGS, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Configure')),
9494
                            'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9495
                        );
9496
9497
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9498
                            $settings = '';
9499
                        }
9500
                        $actions .= $settings;
9501
9502
                        // Exercise results
9503
                        $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9504
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9505
9506
                        if ($limitTeacherAccess) {
9507
                            if (api_is_platform_admin()) {
9508
                                $actions .= $resultsLink;
9509
                            }
9510
                        } else {
9511
                            // Exercise results
9512
                            $actions .= $resultsLink;
9513
                        }
9514
9515
                        // Auto launch
9516
                        $autoLaunch = $exercise->getAutoLaunch();
9517
                        if (empty($autoLaunch)) {
9518
                            $actions .= Display::url(
9519
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('Enable')),
9520
                                'exercise.php?'.api_get_cidreq(
9521
                                ).'&action=enable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9522
                            );
9523
                        } else {
9524
                            $actions .= Display::url(
9525
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Disable')),
9526
                                'exercise.php?'.api_get_cidreq(
9527
                                ).'&action=disable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9528
                            );
9529
                        }
9530
9531
                        // Export
9532
                        $actions .= Display::url(
9533
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9534
                            '',
9535
                            [
9536
                                'onclick' => "javascript:if(!confirm('".addslashes(
9537
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9538
                                    )." ".addslashes($title)."?"."')) return false;",
9539
                                'href' => 'exercise.php?'.api_get_cidreq(
9540
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9541
                            ]
9542
                        );
9543
9544
                        // Clean exercise
9545
                        $clean = '';
9546
                        if (true === $allowClean) {
9547
                            if (!$locked) {
9548
                                $clean = Display::url(
9549
                                    Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Clear all learners results for this exercise')
9550
                                    ),
9551
                                    '',
9552
                                    [
9553
                                        'onclick' => "javascript:if(!confirm('".
9554
                                            addslashes(
9555
                                                api_htmlentities(
9556
                                                    get_lang('Are you sure to delete results'),
9557
                                                    ENT_QUOTES
9558
                                                )
9559
                                            )." ".addslashes($title)."?"."')) return false;",
9560
                                        'href' => 'exercise.php?'.api_get_cidreq(
9561
                                            ).'&action=clean_results&sec_token='.$token.'&exerciseId='.$exerciseId,
9562
                                    ]
9563
                                );
9564
                            } else {
9565
                                $clean = Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('This option is not available because this activity is contained by an assessment, which is currently locked. To unlock the assessment, ask your platform administrator.')
9566
                                );
9567
                            }
9568
                        }
9569
9570
                        $actions .= $clean;
9571
                        // Visible / invisible
9572
                        // Check if this exercise was added in a LP
9573
                        $visibility = '';
9574
                        if (api_is_platform_admin()) {
9575
                            if ($exercise->exercise_was_added_in_lp) {
9576
                                $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('This exercise has been included in a learning path, so it cannot be accessed by students directly from here. If you want to put the same exercise available through the exercises tool, please make a copy of the current exercise using the copy icon.')
9577
                                );
9578
                            } else {
9579
                                if (!$exerciseEntity->isVisible($course, $session)) {
9580
                                    $visibility = Display::url(
9581
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9582
                                        ),
9583
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9584
                                    );
9585
                                } else {
9586
                                    // else if not active
9587
                                    $visibility = Display::url(
9588
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9589
                                        ),
9590
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9591
                                    );
9592
                                }
9593
                            }
9594
                        }
9595
9596
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9597
                            $visibility = '';
9598
                        }
9599
9600
                        $actions .= $visibility;
9601
9602
                        // Export qti ...
9603
                        $export = Display::url(
9604
                            Display::getMdiIcon(
9605
                                'database',
9606
                                'ch-tool-icon',
9607
                                null,
9608
                                ICON_SIZE_SMALL,
9609
                                'IMS/QTI'
9610
                            ),
9611
                            'exercise.php?action=exportqti2&exerciseId='.$exerciseId.'&'.api_get_cidreq()
9612
                        );
9613
9614
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9615
                            $export = '';
9616
                        }
9617
9618
                        $actions .= $export;
9619
                    } else {
9620
                        // not session
9621
                        $actions = Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang("You can't edit this course exercise from inside a session")
9622
                        );
9623
9624
                        // Check if this exercise was added in a LP
9625
                        $visibility = '';
9626
                        if (api_is_platform_admin()) {
9627
                            if ($exercise->exercise_was_added_in_lp) {
9628
                                $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('This exercise has been included in a learning path, so it cannot be accessed by students directly from here. If you want to put the same exercise available through the exercises tool, please make a copy of the current exercise using the copy icon.')
9629
                                );
9630
                            } else {
9631
                                if (!$visibility) {
9632
                                    $visibility = Display::url(
9633
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9634
                                        ),
9635
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9636
                                    );
9637
                                } else {
9638
                                    // else if not active
9639
                                    $visibility = Display::url(
9640
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9641
                                        ),
9642
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9643
                                    );
9644
                                }
9645
                            }
9646
                        }
9647
9648
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9649
                            $visibility = '';
9650
                        }
9651
9652
                        $actions .= $visibility;
9653
                        $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9654
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9655
                        $actions .= Display::url(
9656
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9657
                            '',
9658
                            [
9659
                                'onclick' => "javascript:if(!confirm('".addslashes(
9660
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9661
                                    )." ".addslashes($title)."?"."')) return false;",
9662
                                'href' => 'exercise.php?'.api_get_cidreq(
9663
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9664
                            ]
9665
                        );
9666
                    }
9667
9668
                    // Delete
9669
                    $delete = '';
9670
                    if ($repo->isGranted('DELETE', $exerciseEntity) && $allowToEditBaseCourse) {
9671
                        if (!$locked) {
9672
                            $deleteUrl = 'exercise.php?'.api_get_cidreq().'&action=delete&sec_token='.$token.'&exerciseId='.$exerciseId;
9673
                            $delete = Display::url(
9674
                                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
9675
                                '',
9676
                                [
9677
                                    'onclick' => "javascript:if(!confirm('".
9678
                                        addslashes(api_htmlentities(get_lang('Are you sure you want to delete')))." ".
9679
                                        addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9680
                                    'href' => $deleteUrl,
9681
                                ]
9682
                            );
9683
                        } else {
9684
                            $delete = Display::getMdiIcon(
9685
                                ActionIcon::DELETE,
9686
                                'ch-tool-icon-disabled',
9687
                                null,
9688
                                ICON_SIZE_SMALL,
9689
                                get_lang(
9690
                                    'This option is not available because this activity is contained by an assessment, which is currently locked. To unlock the assessment, ask your platform administrator.'
9691
                                )
9692
                            );
9693
                        }
9694
                    }
9695
9696
                    if ($limitTeacherAccess && !api_is_platform_admin()) {
9697
                        $delete = '';
9698
                    }
9699
9700
                    if (!empty($minCategoriesInExercise)) {
9701
                        $cats = TestCategory::getListOfCategoriesForTest($exercise);
9702
                        if (!(count($cats) >= $minCategoriesInExercise)) {
9703
                            continue;
9704
                        }
9705
                    }
9706
                    $actions .= $delete;
9707
9708
                    // Number of questions.
9709
                    $random = $exerciseEntity->getRandom();
9710
                    if ($random > 0 || -1 == $random) {
9711
                        // if random == -1 means use random questions with all questions
9712
                        $random_number_of_question = $random;
9713
                        if (-1 == $random_number_of_question) {
9714
                            $random_number_of_question = $rowi;
9715
                        }
9716
                        if ($exerciseEntity->getRandomByCategory() > 0) {
9717
                            $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9718
                                $exerciseId,
9719
                                $random_number_of_question
9720
                            );
9721
                            $number_of_questions = $nbQuestionsTotal.' ';
9722
                            $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('questions') : get_lang(
9723
                                'Question lower case'
9724
                            );
9725
                            $number_of_questions .= ' - ';
9726
                            $number_of_questions .= min(
9727
                                    TestCategory::getNumberMaxQuestionByCat($exerciseId),
9728
                                    $random_number_of_question
9729
                                ).' '.get_lang('Question by category');
9730
                        } else {
9731
                            $random_label = ' ('.get_lang('Random').') ';
9732
                            $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9733
                            // Bug if we set a random value bigger than the real number of questions
9734
                            if ($random_number_of_question > $rowi) {
9735
                                $number_of_questions = $rowi.' '.$random_label;
9736
                            }
9737
                        }
9738
                    } else {
9739
                        $number_of_questions = $rowi;
9740
                    }
9741
9742
                    $currentRow['count_questions'] = $number_of_questions;
9743
                } else {
9744
                    // Student only.
9745
                    if (!$visibleForStudent) {
9746
                        continue;
9747
                    }
9748
9749
                    $url = '<a '.$alt_title.'
9750
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'">'.
9751
                        $cut_title.'</a>';
9752
9753
                    // Link of the exercise.
9754
                    $currentRow['title'] = $url.' '.$sessionStar;
9755
                    // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9756
                    if ($returnData) {
9757
                        $currentRow['title'] = $exercise->getUnformattedTitle();
9758
                    }
9759
9760
                    $sessionCondition = api_get_session_condition(api_get_session_id());
9761
                    // Don't remove this marker: note-query-exe-results
9762
                    $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9763
                            WHERE
9764
                                exe_exo_id = ".$exerciseId." AND
9765
                                exe_user_id = $userId AND
9766
                                c_id = ".api_get_course_int_id()." AND
9767
                                status <> 'incomplete' AND
9768
                                orig_lp_id = 0 AND
9769
                                orig_lp_item_id = 0
9770
                                $sessionCondition
9771
                            ORDER BY exe_id DESC";
9772
9773
                    $qryres = Database::query($sql);
9774
                    $num = Database:: num_rows($qryres);
9775
9776
                    // Hide the results.
9777
                    $my_result_disabled = $exerciseEntity->getResultsDisabled();
9778
                    $attempt_text = '-';
9779
                    // Time limits are on
9780
                    if ($time_limits) {
9781
                        // Exam is ready to be taken
9782
                        if ($is_actived_time) {
9783
                            // Show results
9784
                            if (
9785
                            in_array(
9786
                                $my_result_disabled,
9787
                                [
9788
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9789
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9790
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9791
                                    RESULT_DISABLE_RANKING,
9792
                                ]
9793
                            )
9794
                            ) {
9795
                                // More than one attempt
9796
                                if ($num > 0) {
9797
                                    $row_track = Database:: fetch_array($qryres);
9798
                                    $attempt_text = get_lang('Latest attempt').' : ';
9799
                                    $attempt_text .= ExerciseLib::show_score(
9800
                                        $row_track['score'],
9801
                                        $row_track['max_score']
9802
                                    );
9803
                                } else {
9804
                                    //No attempts
9805
                                    $attempt_text = get_lang('Not attempted');
9806
                                }
9807
                            } else {
9808
                                $attempt_text = '-';
9809
                            }
9810
                        } else {
9811
                            // Quiz not ready due to time limits
9812
                            //@todo use the is_visible function
9813
                            if (!empty($startTime) && !empty($endTime)) {
9814
                                $today = time();
9815
                                if ($today < $start_time) {
9816
                                    $attempt_text = sprintf(
9817
                                        get_lang('Exercise will be activated from %s to %s'),
9818
                                        api_convert_and_format_date($start_time),
9819
                                        api_convert_and_format_date($end_time)
9820
                                    );
9821
                                } else {
9822
                                    if ($today > $end_time) {
9823
                                        $attempt_text = sprintf(
9824
                                            get_lang('Exercise was activated from %s to %s'),
9825
                                            api_convert_and_format_date($start_time),
9826
                                            api_convert_and_format_date($end_time)
9827
                                        );
9828
                                    }
9829
                                }
9830
                            } else {
9831
                                if (!empty($startTime)) {
9832
                                    $attempt_text = sprintf(
9833
                                        get_lang('Exercise available from %s'),
9834
                                        api_convert_and_format_date($start_time)
9835
                                    );
9836
                                }
9837
                                if (!empty($endTime)) {
9838
                                    $attempt_text = sprintf(
9839
                                        get_lang('Exercise available until %s'),
9840
                                        api_convert_and_format_date($end_time)
9841
                                    );
9842
                                }
9843
                            }
9844
                        }
9845
                    } else {
9846
                        // Normal behaviour.
9847
                        // Show results.
9848
                        if (
9849
                        in_array(
9850
                            $my_result_disabled,
9851
                            [
9852
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9853
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9854
                                RESULT_DISABLE_SHOW_SCORE_ONLY,
9855
                                RESULT_DISABLE_RANKING,
9856
                                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
9857
                            ]
9858
                        )
9859
                        ) {
9860
                            if ($num > 0) {
9861
                                $row_track = Database::fetch_array($qryres);
9862
                                $attempt_text = get_lang('Latest attempt').' : ';
9863
                                $attempt_text .= ExerciseLib::show_score(
9864
                                    $row_track['score'],
9865
                                    $row_track['max_score']
9866
                                );
9867
                            } else {
9868
                                $attempt_text = get_lang('Not attempted');
9869
                            }
9870
                        }
9871
                    }
9872
                    if ($returnData) {
9873
                        $attempt_text = $num;
9874
                    }
9875
                }
9876
9877
                $currentRow['attempt'] = $attempt_text;
9878
                $currentRow['iid'] = $exerciseId;
9879
9880
                if ($is_allowedToEdit) {
9881
                    $additionalActions = ExerciseLib::getAdditionalTeacherActions($exerciseId);
9882
9883
                    if (!empty($additionalActions)) {
9884
                        $actions .= $additionalActions.PHP_EOL;
9885
                    }
9886
9887
                    if (!empty($myActions) && is_callable($myActions)) {
9888
                        $actions = $myActions($currentRow);
9889
                    }
9890
                    $currentRow = [
9891
                        $exerciseId,
9892
                        $currentRow['title'],
9893
                        $currentRow['count_questions'],
9894
                        $actions,
9895
                    ];
9896
                } else {
9897
                    $currentRow = [
9898
                        $currentRow['title'],
9899
                        $currentRow['attempt'],
9900
                    ];
9901
9902
                    if ($isDrhOfCourse) {
9903
                        $currentRow[] = '<a
9904
                            href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9905
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).
9906
                            '</a>';
9907
                    }
9908
                    if ($returnData) {
9909
                        $currentRow['id'] = $exercise->id;
9910
                        $currentRow['url'] = $webPath.'exercise/overview.php?'
9911
                            .api_get_cidreq().'&'
9912
                            ."$mylpid$mylpitemid&exerciseId={$exercise->id}";
9913
                        $currentRow['name'] = $currentRow[0];
9914
                    }
9915
                }
9916
                $tableRows[] = $currentRow;
9917
            }
9918
        }
9919
9920
        if (empty($tableRows) && empty($categoryId)) {
9921
            if ($is_allowedToEdit && 'learnpath' !== $origin) {
9922
                if (!empty($_GET['keyword'])) {
9923
                    $content .= Display::return_message(
9924
                        sprintf(get_lang('No result for keyword %s'),Security::remove_XSS($_GET['keyword'])),
9925
                        'warning'
9926
                    );
9927
                } else {
9928
                    $content .= Display::noDataView(
9929
                        get_lang('Test'),
9930
                        Display::getMdiIcon(ToolIcon::QUIZ, 'ch-tool-icon', null, ICON_SIZE_BIG),
9931
                        get_lang('Create a new test'),
9932
                        'exercise_admin.php?'.api_get_cidreq()
9933
                    );
9934
                }
9935
            }
9936
        } else {
9937
            if (empty($tableRows)) {
9938
                return '';
9939
            }
9940
            $table->setTableData($tableRows);
9941
            $table->setTotalNumberOfItems($total);
9942
            $table->set_additional_parameters(
9943
                [
9944
                    'cid' => api_get_course_int_id(),
9945
                    'sid' => api_get_session_id(),
9946
                    'category_id' => $categoryId,
9947
                    'sec_token'   => Security::get_token(),
9948
                ]
9949
            );
9950
9951
            if ($is_allowedToEdit) {
9952
                $formActions = [];
9953
                $formActions['visible'] = get_lang('Activate');
9954
                $formActions['invisible'] = get_lang('Deactivate');
9955
                $formActions['delete'] = get_lang('Delete');
9956
                $table->set_form_actions($formActions);
9957
            }
9958
9959
            $i = 0;
9960
            if ($is_allowedToEdit) {
9961
                $table->set_header($i++, '', false, 'width="18px"');
9962
            }
9963
            $table->set_header($i++, get_lang('Test name'), false);
9964
9965
            if ($is_allowedToEdit) {
9966
                $table->set_header($i++, get_lang('Questions'), false, [], ['class' => 'text-center']);
9967
                $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9968
            } else {
9969
                $table->set_header($i++, get_lang('Status'), false);
9970
                if ($isDrhOfCourse) {
9971
                    $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9972
                }
9973
            }
9974
9975
            if ($returnTable) {
9976
                return $table;
9977
            }
9978
            $content .= $table->return_table();
9979
        }
9980
9981
        return $content;
9982
    }
9983
9984
    /**
9985
     * @return int value in minutes
9986
     */
9987
    public function getResultAccess()
9988
    {
9989
        $extraFieldValue = new ExtraFieldValue('exercise');
9990
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9991
            $this->iId,
9992
            'results_available_for_x_minutes'
9993
        );
9994
9995
        if (!empty($value) && isset($value['value'])) {
9996
            return (int) $value['value'];
9997
        }
9998
9999
        return 0;
10000
    }
10001
10002
    /**
10003
     * @param array $exerciseResultInfo
10004
     *
10005
     * @return bool
10006
     */
10007
    public function getResultAccessTimeDiff($exerciseResultInfo)
10008
    {
10009
        $value = $this->getResultAccess();
10010
        if (!empty($value)) {
10011
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
10012
            $endDate->add(new DateInterval('PT'.$value.'M'));
10013
            $now = time();
10014
            if ($endDate->getTimestamp() > $now) {
10015
                return (int) $endDate->getTimestamp() - $now;
10016
            }
10017
        }
10018
10019
        return 0;
10020
    }
10021
10022
    /**
10023
     * @param array $exerciseResultInfo
10024
     *
10025
     * @return bool
10026
     */
10027
    public function hasResultsAccess($exerciseResultInfo)
10028
    {
10029
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
10030
        if (0 === $diff) {
10031
            return false;
10032
        }
10033
10034
        return true;
10035
    }
10036
10037
    /**
10038
     * @return int
10039
     */
10040
    public function getResultsAccess()
10041
    {
10042
        $extraFieldValue = new ExtraFieldValue('exercise');
10043
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
10044
            $this->iId,
10045
            'results_available_for_x_minutes'
10046
        );
10047
        if (!empty($value)) {
10048
            return (int) $value;
10049
        }
10050
10051
        return 0;
10052
    }
10053
10054
    /**
10055
     * @param int   $questionId
10056
     * @param bool  $show_results
10057
     * @param array $question_result
10058
     */
10059
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
10060
    {
10061
        $id = (int) $objQuestionTmp->id;
10062
        $questionId = (int) $questionId;
10063
10064
        $final_overlap = $question_result['extra']['final_overlap'];
10065
        $final_missing = $question_result['extra']['final_missing'];
10066
        $final_excess = $question_result['extra']['final_excess'];
10067
10068
        $overlap_color = $question_result['extra']['overlap_color'];
10069
        $missing_color = $question_result['extra']['missing_color'];
10070
        $excess_color = $question_result['extra']['excess_color'];
10071
10072
        $threadhold1 = $question_result['extra']['threadhold1'];
10073
        $threadhold2 = $question_result['extra']['threadhold2'];
10074
        $threadhold3 = $question_result['extra']['threadhold3'];
10075
10076
        if ($show_results) {
10077
            if ($overlap_color) {
10078
                $overlap_color = 'green';
10079
            } else {
10080
                $overlap_color = 'red';
10081
            }
10082
10083
            if ($missing_color) {
10084
                $missing_color = 'green';
10085
            } else {
10086
                $missing_color = 'red';
10087
            }
10088
            if ($excess_color) {
10089
                $excess_color = 'green';
10090
            } else {
10091
                $excess_color = 'red';
10092
            }
10093
10094
            if (!is_numeric($final_overlap)) {
10095
                $final_overlap = 0;
10096
            }
10097
10098
            if (!is_numeric($final_missing)) {
10099
                $final_missing = 0;
10100
            }
10101
            if (!is_numeric($final_excess)) {
10102
                $final_excess = 0;
10103
            }
10104
10105
            if ($final_excess > 100) {
10106
                $final_excess = 100;
10107
            }
10108
10109
            $table_resume = '
10110
                    <table class="table table-hover table-striped data_table">
10111
                        <tr class="row_odd" >
10112
                            <td>&nbsp;</td>
10113
                            <td><b>'.get_lang('Requirements').'</b></td>
10114
                            <td><b>'.get_lang('Your answer').'</b></td>
10115
                        </tr>
10116
                        <tr class="row_even">
10117
                            <td><b>'.get_lang('Overlapping area').'</b></td>
10118
                            <td>'.get_lang('Minimum overlap').' '.$threadhold1.'</td>
10119
                            <td>
10120
                                <div style="color:'.$overlap_color.'">
10121
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
10122
                                </div>
10123
                            </td>
10124
                        </tr>
10125
                        <tr>
10126
                            <td><b>'.get_lang('Excessive area').'</b></td>
10127
                            <td>'.get_lang('Maximum excess').' '.$threadhold2.'</td>
10128
                            <td>
10129
                                <div style="color:'.$excess_color.'">
10130
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
10131
                                </div>
10132
                            </td>
10133
                        </tr>
10134
                        <tr class="row_even">
10135
                            <td><b>'.get_lang('Missing area').'</b></td>
10136
                            <td>'.get_lang('Maximum missing').' '.$threadhold3.'</td>
10137
                            <td>
10138
                                <div style="color:'.$missing_color.'">
10139
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
10140
                                </div>
10141
                            </td>
10142
                        </tr>
10143
                    </table>
10144
                ';
10145
10146
            $answerType = $objQuestionTmp->selectType();
10147
            /*if ($next == 0) {
10148
                $try = $try_hotspot;
10149
                $lp = $lp_hotspot;
10150
                $destinationid = $select_question_hotspot;
10151
                $url = $url_hotspot;
10152
            } else {
10153
                //show if no error
10154
                $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
10155
                $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
10156
            }
10157
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
10158
            if ($organs_at_risk_hit > 0) {
10159
                $message = '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
10160
                $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
10161
            } else {
10162
                $message = '<p>'.get_lang('Your delineation :').'</p>';
10163
                $message .= $table_resume;
10164
                $message .= '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
10165
            }
10166
            $message .= '<p>'.$comment.'</p>';
10167
            echo $message;*/
10168
10169
            // Showing the score
10170
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
10171
                          WHERE exe_id = $id AND question_id =  $questionId";
10172
            $resfree = Database::query($queryfree);
10173
            $questionScore = Database::result($resfree, 0, 'marks');
10174
            $totalScore += $questionScore;*/
10175
            $relPath = api_get_path(REL_CODE_PATH);
10176
            echo '</table></td></tr>';
10177
            echo "
10178
                        <tr>
10179
                            <td colspan=\"2\">
10180
                                <div id=\"hotspot-solution\"></div>
10181
                                <script>
10182
                                    $(function() {
10183
                                        new HotspotQuestion({
10184
                                            questionId: $questionId,
10185
                                            exerciseId: {$this->id},
10186
                                            exeId: $id,
10187
                                            selector: '#hotspot-solution',
10188
                                            for: 'solution',
10189
                                            relPath: '$relPath'
10190
                                        });
10191
                                    });
10192
                                </script>
10193
                            </td>
10194
                        </tr>
10195
                    </table>
10196
                ";
10197
        }
10198
    }
10199
10200
    /**
10201
     * Clean exercise session variables.
10202
     */
10203
    public static function cleanSessionVariables()
10204
    {
10205
        Session::erase('objExercise');
10206
        Session::erase('exe_id');
10207
        Session::erase('calculatedAnswerId');
10208
        Session::erase('duration_time_previous');
10209
        Session::erase('duration_time');
10210
        Session::erase('objQuestion');
10211
        Session::erase('objAnswer');
10212
        Session::erase('questionList');
10213
        Session::erase('categoryList');
10214
        Session::erase('exerciseResult');
10215
        Session::erase('firstTime');
10216
10217
        Session::erase('time_per_question');
10218
        Session::erase('question_start');
10219
        Session::erase('exerciseResultCoordinates');
10220
        Session::erase('hotspot_coord');
10221
        Session::erase('hotspot_dest');
10222
        Session::erase('hotspot_delineation_result');
10223
    }
10224
10225
    /**
10226
     * Get the first LP found matching the session ID.
10227
     *
10228
     * @param int $sessionId
10229
     *
10230
     * @return array
10231
     */
10232
    public function getLpBySession($sessionId)
10233
    {
10234
        if (!empty($this->lpList)) {
10235
            $sessionId = (int) $sessionId;
10236
10237
            foreach ($this->lpList as $lp) {
10238
                if (isset($lp['session_id']) && (int) $lp['session_id'] == $sessionId) {
10239
                    return $lp;
10240
                }
10241
            }
10242
10243
            return current($this->lpList);
10244
        }
10245
10246
        return [
10247
            'lp_id' => 0,
10248
            'max_score' => 0,
10249
            'session_id' => 0,
10250
        ];
10251
    }
10252
10253
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id, $course_id = null)
10254
    {
10255
        $lp = Session::read('oLP');
10256
10257
        $safe_exe_id = (int) $safe_exe_id;
10258
        $safe_item_id = (int) $safe_item_id;
10259
10260
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
10261
            return false;
10262
        }
10263
10264
        $viewId = $lp->get_view_id();
10265
        if (!isset($course_id)) {
10266
            $course_id = api_get_course_int_id();
10267
        }
10268
        $userId = (int) api_get_user_id();
10269
        $viewId = (int) $viewId;
10270
10271
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
10272
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
10273
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
10274
10275
        $sql = "SELECT start_date, exe_date, score, max_score, exe_exo_id, exe_duration
10276
                FROM $TBL_TRACK_EXERCICES
10277
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
10278
        $res = Database::query($sql);
10279
        $row_dates = Database::fetch_array($res);
10280
10281
        if (empty($row_dates)) {
10282
            return false;
10283
        }
10284
10285
        $duration = (int) $row_dates['exe_duration'];
10286
        $score = (float) $row_dates['score'];
10287
        $max_score = (float) $row_dates['max_score'];
10288
10289
        $sql = "UPDATE $TBL_LP_ITEM SET
10290
                    max_score = '$max_score'
10291
                WHERE iid = $safe_item_id";
10292
        Database::query($sql);
10293
10294
        $sql = "SELECT iid FROM $TBL_LP_ITEM_VIEW
10295
                WHERE
10296
                    lp_item_id = $safe_item_id AND
10297
                    lp_view_id = $viewId
10298
                ORDER BY iid DESC
10299
                LIMIT 1";
10300
        $res_last_attempt = Database::query($sql);
10301
10302
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
10303
            $row_last_attempt = Database::fetch_row($res_last_attempt);
10304
            $lp_item_view_id = $row_last_attempt[0];
10305
10306
            $exercise = new Exercise($course_id);
10307
            $exercise->read($row_dates['exe_exo_id']);
10308
            $status = 'completed';
10309
10310
            if (!empty($exercise->pass_percentage)) {
10311
                $status = 'failed';
10312
                $success = ExerciseLib::isSuccessExerciseResult(
10313
                    $score,
10314
                    $max_score,
10315
                    $exercise->pass_percentage
10316
                );
10317
                if ($success) {
10318
                    $status = 'passed';
10319
                }
10320
            }
10321
10322
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
10323
                        status = '$status',
10324
                        score = '$score',
10325
                        total_time = '$duration'
10326
                    WHERE iid = $lp_item_view_id";
10327
            Database::query($sql);
10328
10329
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
10330
                        orig_lp_item_view_id = '$lp_item_view_id'
10331
                    WHERE exe_id = ".$safe_exe_id;
10332
            Database::query($sql);
10333
        }
10334
    }
10335
10336
    /**
10337
     * Get the user answers saved in exercise.
10338
     *
10339
     * @param int $attemptId
10340
     *
10341
     * @return array
10342
     */
10343
    public function getUserAnswersSavedInExercise($attemptId)
10344
    {
10345
        $exerciseResult = [];
10346
10347
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
10348
10349
        foreach ($attemptList as $questionId => $options) {
10350
            foreach ($options as $option) {
10351
                $question = Question::read($option['question_id']);
10352
10353
                if ($question) {
10354
                    switch ($question->type) {
10355
                        case FILL_IN_BLANKS:
10356
                        case FILL_IN_BLANKS_COMBINATION:
10357
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
10358
                            if ($option['answer'] === "0") {
10359
                                $option['answer'] = "there is 0 as answer so we do not want to consider it empty";
10360
                            }
10361
                            break;
10362
                    }
10363
                }
10364
10365
                if (!empty($option['answer'])) {
10366
                    $exerciseResult[] = $questionId;
10367
10368
                    break;
10369
                }
10370
            }
10371
        }
10372
10373
        return $exerciseResult;
10374
    }
10375
10376
    /**
10377
     * Get the number of user answers saved in exercise.
10378
     *
10379
     * @param int $attemptId
10380
     *
10381
     * @return int
10382
     */
10383
    public function countUserAnswersSavedInExercise($attemptId)
10384
    {
10385
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
10386
10387
        return count($answers);
10388
    }
10389
10390
    public static function allowAction($action)
10391
    {
10392
        if (api_is_platform_admin()) {
10393
            return true;
10394
        }
10395
10396
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
10397
        $disableClean = ('true' === api_get_setting('exercise.disable_clean_exercise_results_for_teachers'));
10398
10399
        switch ($action) {
10400
            case 'delete':
10401
                if (api_is_allowed_to_edit(null, true)) {
10402
                    if ($limitTeacherAccess) {
10403
                        return false;
10404
                    }
10405
10406
                    return true;
10407
                }
10408
                break;
10409
            case 'clean_results':
10410
                if (api_is_allowed_to_edit(null, true)) {
10411
                    if ($limitTeacherAccess) {
10412
                        return false;
10413
                    }
10414
10415
                    if ($disableClean) {
10416
                        return false;
10417
                    }
10418
10419
                    return true;
10420
                }
10421
10422
                break;
10423
        }
10424
10425
        return false;
10426
    }
10427
10428
    public static function getLpListFromExercise($exerciseId, $courseId)
10429
    {
10430
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
10431
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
10432
10433
        $exerciseId = (int) $exerciseId;
10434
        $courseId = (int) $courseId;
10435
10436
        $sql = "SELECT
10437
                    lp.title,
10438
                    lpi.lp_id,
10439
                    lpi.max_score
10440
                FROM $tableLpItem lpi
10441
                INNER JOIN $tblLp lp
10442
                ON (lpi.lp_id = lp.iid)
10443
                WHERE
10444
                    lpi.item_type = '".TOOL_QUIZ."' AND
10445
                    lpi.path = '$exerciseId'";
10446
        $result = Database::query($sql);
10447
        $lpList = [];
10448
        if (Database::num_rows($result) > 0) {
10449
            $lpList = Database::store_result($result, 'ASSOC');
10450
        }
10451
10452
        return $lpList;
10453
    }
10454
10455
    /**
10456
     * Generate reminder table with robust checkbox markup (no external CSS dependency).
10457
     */
10458
    function getReminderTable($questionList, $exercise_stat_info, $disableCheckBoxes = false)
10459
    {
10460
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10461
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10462
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10463
        $categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
10464
10465
        if (empty($exercise_stat_info)) {
10466
            return '';
10467
        }
10468
10469
        $remindList = $exercise_stat_info['questions_to_check'];
10470
        $remindList = explode(',', $remindList);
10471
10472
        $exeId = $exercise_stat_info['exe_id'];
10473
        $exerciseId = $exercise_stat_info['exe_exo_id'];
10474
        $exercise_result = Exercise::getUserAnswersSavedInExerciseStatic($exeId); // Static helper to avoid $this here
10475
10476
        $content = Display::label(get_lang('Questions without answer'), 'danger');
10477
        $content .= '<div class="clear"></div><br />';
10478
        $table = '';
10479
        $counter = 0;
10480
10481
        // Loop over all questions
10482
        foreach ($questionList as $questionId) {
10483
            $objQuestionTmp = Question::read($questionId);
10484
            $check_id = 'remind_list['.$questionId.']';
10485
            $attributes = [
10486
                'id' => $check_id,
10487
                'onclick' => "save_remind_item(this, '$questionId');",
10488
                'data-question-id' => $questionId,
10489
            ];
10490
            if (in_array($questionId, $remindList)) {
10491
                $attributes['checked'] = 1;
10492
            }
10493
10494
            // Simple, robust checkbox (no pretty-checkbox wrapper)
10495
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '1', $attributes);
10496
10497
            $counter++;
10498
            $titleText = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
10499
10500
            // Mark unanswered questions
10501
            if (!in_array($questionId, $exercise_result)) {
10502
                $titleHtml = Display::label($titleText, 'danger');
10503
            } else {
10504
                $titleHtml = $titleText;
10505
            }
10506
10507
            $label_attributes = ['for' => $check_id];
10508
10509
            if (false === $disableCheckBoxes) {
10510
                // Combine checkbox + title inside a label; Tailwind classes are no-op if not present
10511
                $rowHtml = Display::tag(
10512
                    'label',
10513
                    $checkbox.' '.$titleHtml,
10514
                    $label_attributes
10515
                );
10516
            } else {
10517
                $rowHtml = $titleHtml;
10518
            }
10519
10520
            // Add container with alignment classes (works w/ or w/o Tailwind thanks to inline CSS above)
10521
            $table .= Display::div($rowHtml, ['class' => 'exercise_reminder_item flex items-center gap-2']);
10522
        }
10523
10524
        // JS functions
10525
        $content .= Display::div('', ['id' => 'message']).
10526
            Display::div($table, ['class' => 'question-check-test']);
10527
10528
        $content .= '<script>
10529
    var lp_data = $.param({
10530
        "learnpath_id": '.$learnpath_id.',
10531
        "learnpath_item_id" : '.$learnpath_item_id.',
10532
        "learnpath_item_view_id": '.$learnpath_item_view_id.'
10533
    });
10534
10535
    function final_submit() {
10536
        // Normal inputs.
10537
        window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
10538
    }
10539
10540
    function changeOptionStatus(status)
10541
    {
10542
        $("input[type=checkbox]").each(function () {
10543
            $(this).prop("checked", status);
10544
        });
10545
10546
        var action = "";
10547
        var option = "remove_all";
10548
        if (status == 1) {
10549
            option = "add_all";
10550
        }
10551
        $.ajax({
10552
            url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10553
            data: "option="+option+"&exe_id='.$exeId.'&action="+action,
10554
            success: function(returnValue) {}
10555
        });
10556
    }
10557
10558
    function reviewQuestions() {
10559
        var hasChecked = false;
10560
        $("input[type=checkbox]").each(function () {
10561
            if ($(this).prop("checked")) {
10562
                hasChecked = true;
10563
                return false;
10564
            }
10565
        });
10566
10567
        if (!hasChecked) {
10568
            $("#message").addClass("warning-message");
10569
            $("#message").html("'.addslashes(get_lang('Select a question to revise')).'");
10570
        } else {
10571
            window.location = "exercise_submit.php?'.api_get_cidreq().'&category_id='.$categoryId.'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
10572
        }
10573
    }
10574
10575
    // Backward-compat alias (the button calls review_questions())
10576
    function review_questions() {
10577
        return reviewQuestions();
10578
    }
10579
10580
    function save_remind_item(obj, question_id) {
10581
        var action = $(obj).prop("checked") ? "add" : "delete";
10582
        $.ajax({
10583
            url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10584
            data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10585
            success: function(returnValue) {}
10586
        });
10587
    }
10588
    </script>';
10589
10590
        return $content;
10591
    }
10592
10593
    public static function getUserAnswersSavedInExerciseStatic($exeId)
10594
    {
10595
        $objExercise = Session::read("objExercise");
10596
        if ($objExercise && method_exists($objExercise, "getUserAnswersSavedInExercise")) {
10597
            return $objExercise->getUserAnswersSavedInExercise($exeId);
10598
        }
10599
10600
        return [];
10601
    }
10602
10603
    public function getRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10604
    {
10605
        $dataSet = [];
10606
        $labels = [];
10607
        $labelsWithId = [];
10608
        /** @var Exercise $exercise */
10609
        foreach ($exercises as $exercise) {
10610
            if (empty($labels)) {
10611
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10612
                if (!empty($categoryNameList)) {
10613
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10614
                    asort($labelsWithId);
10615
                    $labels = array_values($labelsWithId);
10616
                }
10617
            }
10618
10619
            foreach ($userList as $userId) {
10620
                $results = Event::getExerciseResultsByUser(
10621
                    $userId,
10622
                    $exercise->iId,
10623
                    $courseId,
10624
                    $sessionId
10625
                );
10626
10627
                if ($results) {
10628
                    $firstAttempt = end($results);
10629
                    $exeId = $firstAttempt['exe_id'];
10630
10631
                    ob_start();
10632
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10633
                        $exercise,
10634
                        $exeId,
10635
                        false
10636
                    );
10637
                    ob_end_clean();
10638
10639
                    $categoryList = $stats['category_list'];
10640
                    $tempResult = [];
10641
                    foreach ($labelsWithId as $category_id => $title) {
10642
                        if (isset($categoryList[$category_id])) {
10643
                            $category_item = $categoryList[$category_id];
10644
                            $tempResult[] = round($category_item['score'] / $category_item['total'] * 10);
10645
                        } else {
10646
                            $tempResult[] = 0;
10647
                        }
10648
                    }
10649
                    $dataSet[] = $tempResult;
10650
                }
10651
            }
10652
        }
10653
10654
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10655
    }
10656
10657
    public function getAverageRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10658
    {
10659
        $dataSet = [];
10660
        $labels = [];
10661
        $labelsWithId = [];
10662
10663
        $tempResult = [];
10664
        /** @var Exercise $exercise */
10665
        foreach ($exercises as $exercise) {
10666
            $exerciseId = $exercise->iId;
10667
            if (empty($labels)) {
10668
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10669
                if (!empty($categoryNameList)) {
10670
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10671
                    asort($labelsWithId);
10672
                    $labels = array_values($labelsWithId);
10673
                }
10674
            }
10675
10676
            foreach ($userList as $userId) {
10677
                $results = Event::getExerciseResultsByUser(
10678
                    $userId,
10679
                    $exerciseId,
10680
                    $courseId,
10681
                    $sessionId
10682
                );
10683
10684
                if ($results) {
10685
                    $firstAttempt = end($results);
10686
                    $exeId = $firstAttempt['exe_id'];
10687
10688
                    ob_start();
10689
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10690
                        $exercise,
10691
                        $exeId,
10692
                        false
10693
                    );
10694
                    ob_end_clean();
10695
10696
                    $categoryList = $stats['category_list'];
10697
                    foreach ($labelsWithId as $category_id => $title) {
10698
                        if (isset($categoryList[$category_id])) {
10699
                            $category_item = $categoryList[$category_id];
10700
                            if (!isset($tempResult[$exerciseId][$category_id])) {
10701
                                $tempResult[$exerciseId][$category_id] = 0;
10702
                            }
10703
                            $tempResult[$exerciseId][$category_id] += $category_item['score'] / $category_item['total'] * 10;
10704
                        }
10705
                    }
10706
                }
10707
            }
10708
        }
10709
10710
        $totalUsers = count($userList);
10711
10712
        foreach ($exercises as $exercise) {
10713
            $exerciseId = $exercise->iId;
10714
            $data = [];
10715
            foreach ($labelsWithId as $category_id => $title) {
10716
                if (isset($tempResult[$exerciseId]) && isset($tempResult[$exerciseId][$category_id])) {
10717
                    $data[] = round($tempResult[$exerciseId][$category_id] / $totalUsers);
10718
                } else {
10719
                    $data[] = 0;
10720
                }
10721
            }
10722
            $dataSet[] = $data;
10723
        }
10724
10725
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10726
    }
10727
10728
    public function getRadar($labels, $dataSet, $dataSetLabels = [])
10729
    {
10730
        if (empty($labels) || empty($dataSet)) {
10731
            return '';
10732
        }
10733
10734
        $displayLegend = 0;
10735
        if (!empty($dataSetLabels)) {
10736
            $displayLegend = 1;
10737
        }
10738
10739
        $labels = json_encode($labels);
10740
10741
        $colorList = ChamiloHelper::getColorPalette(true, true);
10742
10743
        $dataSetToJson = [];
10744
        $counter = 0;
10745
        foreach ($dataSet as $index => $resultsArray) {
10746
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0, 255).', '.rand(0, 255).', '.rand(0, 255).', 1.0)';
10747
10748
            $label = isset($dataSetLabels[$index]) ? $dataSetLabels[$index] : '';
10749
            $background = str_replace('1.0', '0.2', $color);
10750
            $dataSetToJson[] = [
10751
                'fill' => false,
10752
                'label' => $label,
10753
                'backgroundColor' => $background,
10754
                'borderColor' => $color,
10755
                'pointBackgroundColor' => $color,
10756
                'pointBorderColor' => '#fff',
10757
                'pointHoverBackgroundColor' => '#fff',
10758
                'pointHoverBorderColor' => $color,
10759
                'pointRadius' => 6,
10760
                'pointBorderWidth' => 3,
10761
                'pointHoverRadius' => 10,
10762
                'data' => $resultsArray,
10763
            ];
10764
            $counter++;
10765
        }
10766
        $resultsToJson = json_encode($dataSetToJson);
10767
10768
        return "
10769
                <canvas id='categoryRadar' height='200'></canvas>
10770
                <script>
10771
                    var data = {
10772
                        labels: $labels,
10773
                        datasets: $resultsToJson
10774
                    }
10775
                    var options = {
10776
                        responsive: true,
10777
                        scale: {
10778
                            angleLines: {
10779
                                display: false
10780
                            },
10781
                            ticks: {
10782
                                beginAtZero: true,
10783
                                  min: 0,
10784
                                  max: 10,
10785
                                stepSize: 1,
10786
                            },
10787
                            pointLabels: {
10788
                              fontSize: 14,
10789
                              //fontStyle: 'bold'
10790
                            },
10791
                        },
10792
                        elements: {
10793
                            line: {
10794
                                tension: 0,
10795
                                borderWidth: 3
10796
                            }
10797
                        },
10798
                        legend: {
10799
                            //position: 'bottom'
10800
                            display: $displayLegend
10801
                        },
10802
                        animation: {
10803
                            animateScale: true,
10804
                            animateRotate: true
10805
                        },
10806
                    };
10807
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
10808
                    var myRadarChart = new Chart(ctx, {
10809
                        type: 'radar',
10810
                        data: data,
10811
                        options: options
10812
                    });
10813
                </script>
10814
                ";
10815
    }
10816
10817
    /**
10818
     * Returns true if the exercise is locked by percentage. an exercise attempt must be passed.
10819
     */
10820
    public function isBlockedByPercentage(array $attempt = []): bool
10821
    {
10822
        if (empty($attempt)) {
10823
            return false;
10824
        }
10825
        $extraFieldValue = new ExtraFieldValue('exercise');
10826
        $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
10827
            $this->iId,
10828
            'blocking_percentage'
10829
        );
10830
10831
        if (empty($blockExercise['value'])) {
10832
            return false;
10833
        }
10834
10835
        $blockPercentage = (int) $blockExercise['value'];
10836
10837
        if (0 === $blockPercentage) {
10838
            return false;
10839
        }
10840
10841
        $resultPercentage = 0;
10842
10843
        if (isset($attempt['score']) && isset($attempt['max_score'])) {
10844
            $weight = (int) $attempt['max_score'];
10845
            $weight = (0 == $weight) ? 1 : $weight;
10846
            $resultPercentage = float_format(
10847
                ($attempt['score'] / $weight) * 100,
10848
                1
10849
            );
10850
        }
10851
        if ($resultPercentage <= $blockPercentage) {
10852
            return true;
10853
        }
10854
10855
        return false;
10856
    }
10857
10858
    /**
10859
     * Gets the question list ordered by the question_order setting (drag and drop).
10860
     *
10861
     * @param bool $adminView Optional.
10862
     *
10863
     * @return array
10864
     */
10865
    public function getQuestionOrderedList($adminView = false)
10866
    {
10867
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
10868
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
10869
10870
        // Getting question_order to verify that the question
10871
        // list is correct and all question_order's were set
10872
        $sql = "SELECT DISTINCT count(e.question_order) as count
10873
                FROM $TBL_EXERCICE_QUESTION e
10874
                INNER JOIN $TBL_QUESTIONS q
10875
                ON (e.question_id = q.iid)
10876
                WHERE
10877
                  e.quiz_id	= ".$this->getId();
10878
10879
        $result = Database::query($sql);
10880
        $row = Database::fetch_array($result);
10881
        $count_question_orders = $row['count'];
10882
10883
        // Getting question list from the order (question list drag n drop interface).
10884
        $sql = "SELECT DISTINCT e.question_id, e.question_order
10885
                FROM $TBL_EXERCICE_QUESTION e
10886
                INNER JOIN $TBL_QUESTIONS q
10887
                ON (e.question_id = q.iid)
10888
                WHERE
10889
10890
                    e.quiz_id = '".$this->getId()."'
10891
                ORDER BY question_order";
10892
        $result = Database::query($sql);
10893
10894
        // Fills the array with the question ID for this exercise
10895
        // the key of the array is the question position
10896
        $temp_question_list = [];
10897
        $counter = 1;
10898
        $questionList = [];
10899
        while ($new_object = Database::fetch_object($result)) {
10900
            if (!$adminView) {
10901
                // Correct order.
10902
                $questionList[$new_object->question_order] = $new_object->question_id;
10903
            } else {
10904
                $questionList[$counter] = $new_object->question_id;
10905
            }
10906
10907
            // Just in case we save the order in other array
10908
            $temp_question_list[$counter] = $new_object->question_id;
10909
            $counter++;
10910
        }
10911
10912
        if (!empty($temp_question_list)) {
10913
            /* If both array don't match it means that question_order was not correctly set
10914
               for all questions using the default mysql order */
10915
            if (count($temp_question_list) != $count_question_orders) {
10916
                $questionList = $temp_question_list;
10917
            }
10918
        }
10919
10920
        return $questionList;
10921
    }
10922
10923
    /**
10924
     * Get number of questions in exercise by user attempt.
10925
     *
10926
     * @return int
10927
     */
10928
    private function countQuestionsInExercise()
10929
    {
10930
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10931
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10932
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10933
10934
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
10935
10936
        if (!empty($trackInfo)) {
10937
            $questionIds = explode(',', $trackInfo['data_tracking']);
10938
10939
            return count($questionIds);
10940
        }
10941
10942
        return $this->getQuestionCount();
10943
    }
10944
10945
    /**
10946
     * Select N values from the questions per category array.
10947
     *
10948
     * @param array $categoriesAddedInExercise
10949
     * @param array $question_list
10950
     * @param array $questions_by_category
10951
     * @param bool  $flatResult
10952
     * @param bool  $randomizeQuestions
10953
     * @param array $questionsByCategoryMandatory
10954
     *
10955
     * @return array
10956
     */
10957
    private function pickQuestionsPerCategory(
10958
        $categoriesAddedInExercise,
10959
        $question_list,
10960
        &$questions_by_category,
10961
        $flatResult = true,
10962
        $randomizeQuestions = false,
10963
        $questionsByCategoryMandatory = []
10964
    ) {
10965
        $addAll = true;
10966
        $categoryCountArray = [];
10967
10968
        // Getting how many questions will be selected per category.
10969
        if (!empty($categoriesAddedInExercise)) {
10970
            $addAll = false;
10971
            // Parsing question according the category rel exercise settings
10972
            foreach ($categoriesAddedInExercise as $category_info) {
10973
                $category_id = $category_info['category_id'];
10974
                if (isset($questions_by_category[$category_id])) {
10975
                    // How many question will be picked from this category.
10976
                    $count = $category_info['count_questions'];
10977
                    // -1 means all questions
10978
                    $categoryCountArray[$category_id] = $count;
10979
                    if (-1 == $count) {
10980
                        $categoryCountArray[$category_id] = 999;
10981
                    }
10982
                }
10983
            }
10984
        }
10985
10986
        if (!empty($questions_by_category)) {
10987
            $temp_question_list = [];
10988
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
10989
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
10990
                    $numberOfQuestions = 0;
10991
                    if (isset($categoryCountArray[$category_id])) {
10992
                        $numberOfQuestions = $categoryCountArray[$category_id];
10993
                    }
10994
                }
10995
10996
                if ($addAll) {
10997
                    $numberOfQuestions = 999;
10998
                }
10999
                if (!empty($numberOfQuestions)) {
11000
                    $mandatoryQuestions = [];
11001
                    if (isset($questionsByCategoryMandatory[$category_id])) {
11002
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
11003
                    }
11004
11005
                    $elements = TestCategory::getNElementsFromArray(
11006
                        $categoryQuestionList,
11007
                        $numberOfQuestions,
11008
                        $randomizeQuestions,
11009
                        $mandatoryQuestions
11010
                    );
11011
11012
                    if (!empty($elements)) {
11013
                        $temp_question_list[$category_id] = $elements;
11014
                        $categoryQuestionList = $elements;
11015
                    }
11016
                }
11017
            }
11018
11019
            if (!empty($temp_question_list)) {
11020
                if ($flatResult) {
11021
                    $temp_question_list = array_flatten($temp_question_list);
11022
                }
11023
                $question_list = $temp_question_list;
11024
            }
11025
        }
11026
11027
        return $question_list;
11028
    }
11029
11030
    /**
11031
     * Sends a notification when a user ends an examn.
11032
     *
11033
     * @param array  $question_list_answers
11034
     * @param string $origin
11035
     * @param array  $user_info
11036
     * @param string $url_email
11037
     * @param array  $teachers
11038
     */
11039
    private function sendNotificationForOpenQuestions(
11040
        $question_list_answers,
11041
        $origin,
11042
        $user_info,
11043
        $url_email,
11044
        $teachers
11045
    ) {
11046
        // Email configuration settings
11047
        $courseCode = api_get_course_id();
11048
        $courseInfo = api_get_course_info($courseCode);
11049
        $sessionId = api_get_session_id();
11050
        $sessionData = '';
11051
        if (!empty($sessionId)) {
11052
            $sessionInfo = api_get_session_info($sessionId);
11053
            if (!empty($sessionInfo)) {
11054
                $sessionData = '<tr>'
11055
                    .'<td><em>'.get_lang('Session name').'</em></td>'
11056
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
11057
                    .'</tr>';
11058
            }
11059
        }
11060
11061
        $msg = get_lang('A learner has answered an open question').'<br /><br />'
11062
            .get_lang('Attempt details').' : <br /><br />'
11063
            .'<table>'
11064
            .'<tr>'
11065
            .'<td><em>'.get_lang('Course name').'</em></td>'
11066
            .'<td>&nbsp;<b>#course#</b></td>'
11067
            .'</tr>'
11068
            .$sessionData
11069
            .'<tr>'
11070
            .'<td>'.get_lang('Test attempted').'</td>'
11071
            .'<td>&nbsp;#exercise#</td>'
11072
            .'</tr>'
11073
            .'<tr>'
11074
            .'<td>'.get_lang('Learner name').'</td>'
11075
            .'<td>&nbsp;#firstName# #lastName#</td>'
11076
            .'</tr>'
11077
            .'<tr>'
11078
            .'<td>'.get_lang('Learner e-mail').'</td>'
11079
            .'<td>&nbsp;#mail#</td>'
11080
            .'</tr>'
11081
            .'</table>';
11082
11083
        $open_question_list = null;
11084
        foreach ($question_list_answers as $item) {
11085
            $question = $item['question'];
11086
            $answer = $item['answer'];
11087
            $answer_type = $item['answer_type'];
11088
11089
            if (!empty($question) && !empty($answer) && FREE_ANSWER == $answer_type) {
11090
                $open_question_list .=
11091
                    '<tr>
11092
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
11093
                    <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
11094
                    </tr>
11095
                    <tr>
11096
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
11097
                    <td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>
11098
                    </tr>';
11099
            }
11100
        }
11101
11102
        if (!empty($open_question_list)) {
11103
            $msg .= '<p><br />'.get_lang('A learner has answered an open question').' :</p>'.
11104
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
11105
            $msg .= $open_question_list;
11106
            $msg .= '</table><br />';
11107
11108
            $msg = str_replace('#exercise#', $this->exercise, $msg);
11109
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
11110
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
11111
            $msg = str_replace('#mail#', $user_info['email'], $msg);
11112
            $msg = str_replace(
11113
                '#course#',
11114
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?sid='.$sessionId),
11115
                $msg
11116
            );
11117
11118
            if ('learnpath' !== $origin) {
11119
                $msg .= '<br /><a href="#url#">'.get_lang(
11120
                        'Click this link to check the answer and/or give feedback'
11121
                    ).'</a>';
11122
            }
11123
            $msg = str_replace('#url#', $url_email, $msg);
11124
            $subject = get_lang('A learner has answered an open question');
11125
11126
            if (!empty($teachers)) {
11127
                foreach ($teachers as $user_id => $teacher_data) {
11128
                    MessageManager::send_message_simple(
11129
                        $user_id,
11130
                        $subject,
11131
                        $msg
11132
                    );
11133
                }
11134
            }
11135
        }
11136
    }
11137
11138
    /**
11139
     * Send notification for oral questions.
11140
     *
11141
     * @param array  $question_list_answers
11142
     * @param string $origin
11143
     * @param int    $exe_id
11144
     * @param array  $user_info
11145
     * @param string $url_email
11146
     * @param array  $teachers
11147
     */
11148
    private function sendNotificationForOralQuestions(
11149
        $question_list_answers,
11150
        $origin,
11151
        $exe_id,
11152
        $user_info,
11153
        $url_email,
11154
        $teachers
11155
    ): void {
11156
11157
        // Email configuration settings
11158
        $courseCode = api_get_course_id();
11159
        $courseInfo = api_get_course_info($courseCode);
11160
        $oral_question_list = null;
11161
        foreach ($question_list_answers as $item) {
11162
            $question = $item['question'];
11163
            $file = $item['generated_oral_file'];
11164
            $answer = $item['answer'];
11165
            if (0 == $answer) {
11166
                $answer = '';
11167
            }
11168
            $answer_type = $item['answer_type'];
11169
            if (!empty($question) && (!empty($answer) || !empty($file)) && ORAL_EXPRESSION == $answer_type) {
11170
                $oral_question_list .= '<br />
11171
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
11172
                    <tr>
11173
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
11174
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
11175
                    </tr>
11176
                    <tr>
11177
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
11178
                        <td valign="top" bgcolor="#F3F3F3"><p>'.$answer.'</p><p>'.$file.'</p></td>
11179
                    </tr></table>';
11180
            }
11181
        }
11182
11183
        if (!empty($oral_question_list)) {
11184
            $msg = get_lang('A learner has attempted one or more oral question').'<br /><br />
11185
                    '.get_lang('Attempt details').' : <br /><br />
11186
                    <table>
11187
                        <tr>
11188
                            <td><em>'.get_lang('Course name').'</em></td>
11189
                            <td>&nbsp;<b>#course#</b></td>
11190
                        </tr>
11191
                        <tr>
11192
                            <td>'.get_lang('Test attempted').'</td>
11193
                            <td>&nbsp;#exercise#</td>
11194
                        </tr>
11195
                        <tr>
11196
                            <td>'.get_lang('Learner name').'</td>
11197
                            <td>&nbsp;#firstName# #lastName#</td>
11198
                        </tr>
11199
                        <tr>
11200
                            <td>'.get_lang('Learner e-mail').'</td>
11201
                            <td>&nbsp;#mail#</td>
11202
                        </tr>
11203
                    </table>';
11204
            $msg .= '<br />'.sprintf(
11205
                    get_lang('The attempted oral questions are %s'),
11206
                    $oral_question_list
11207
                ).'<br />';
11208
            $msg1 = str_replace('#exercise#', $this->exercise, $msg);
11209
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg1);
11210
            $msg1 = str_replace('#lastName#', $user_info['lastname'], $msg);
11211
            $msg = str_replace('#mail#', $user_info['email'], $msg1);
11212
            $msg1 = str_replace('#course#', $courseInfo['name'], $msg);
11213
11214
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
11215
                $msg1 .= '<br /><a href="#url#">'.get_lang(
11216
                        'Click this link to check the answer and/or give feedback'
11217
                    ).'</a>';
11218
            }
11219
            $msg = str_replace('#url#', $url_email, $msg1);
11220
            $mail_content = $msg;
11221
            $subject = get_lang('A learner has attempted one or more oral question');
11222
11223
            if (!empty($teachers)) {
11224
                foreach ($teachers as $user_id => $teacher_data) {
11225
                    MessageManager::send_message_simple(
11226
                        $user_id,
11227
                        $subject,
11228
                        $mail_content
11229
                    );
11230
                }
11231
            }
11232
        }
11233
    }
11234
11235
    /**
11236
     * Returns an array with the media list.
11237
     *
11238
     * @param array $questionList question list
11239
     *
11240
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
11241
     * <code>
11242
     * array (size=2)
11243
     *  999 =>
11244
     *    array (size=3)
11245
     *      0 => int 7
11246
     *      1 => int 6
11247
     *      2 => int 3254
11248
     *  100 =>
11249
     *   array (size=1)
11250
     *      0 => int 5
11251
     *  </code>
11252
     */
11253
    private function setMediaList($questionList)
11254
    {
11255
        $mediaList = [];
11256
        /*
11257
         * Media feature is not activated in 1.11.x
11258
        if (!empty($questionList)) {
11259
            foreach ($questionList as $questionId) {
11260
                $objQuestionTmp = Question::read($questionId, $this->course_id);
11261
                // If a media question exists
11262
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
11263
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
11264
                } else {
11265
                    // Always the last item
11266
                    $mediaList[999][] = $objQuestionTmp->id;
11267
                }
11268
            }
11269
        }*/
11270
11271
        $this->mediaList = $mediaList;
11272
    }
11273
11274
    /**
11275
     * @return HTML_QuickForm_group
11276
     */
11277
    private function setResultDisabledGroup(FormValidator $form)
11278
    {
11279
        $resultDisabledGroup = [];
11280
11281
        $resultDisabledGroup[] = $form->createElement(
11282
            'radio',
11283
            'results_disabled',
11284
            null,
11285
            get_lang('Auto-evaluation mode: show score and expected answers'),
11286
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
11287
            ['id' => 'result_disabled_0']
11288
        );
11289
11290
        $warning = sprintf(
11291
            get_lang("The setting \"%s\" will change to \"%s\""),
11292
            get_lang('Feedback'),
11293
            get_lang('Exam (no feedback)')
11294
        );
11295
        $resultDisabledGroup[] = $form->createElement(
11296
            'radio',
11297
            'results_disabled',
11298
            null,
11299
            get_lang('Exam mode: Do not show score nor answers'),
11300
            RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS,
11301
            [
11302
                'id' => 'result_disabled_1',
11303
                //'onclick' => 'check_results_disabled()'
11304
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11305
            ]
11306
        );
11307
11308
        $resultDisabledGroup[] = $form->createElement(
11309
            'radio',
11310
            'results_disabled',
11311
            null,
11312
            get_lang('Practice mode: Show score only, by category if at least one is used'),
11313
            RESULT_DISABLE_SHOW_SCORE_ONLY,
11314
            [
11315
                'id' => 'result_disabled_2',
11316
                //'onclick' => 'check_results_disabled()'
11317
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11318
            ]
11319
        );
11320
11321
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
11322
            return $form->addGroup(
11323
                $resultDisabledGroup,
11324
                null,
11325
                get_lang(
11326
                    'Show score to learner'
11327
                )
11328
            );
11329
        }
11330
11331
        $resultDisabledGroup[] = $form->createElement(
11332
            'radio',
11333
            'results_disabled',
11334
            null,
11335
            get_lang(
11336
                'Show score on every attempt, show correct answers only on last attempt (only works with an attempts limit)'
11337
            ),
11338
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
11339
            ['id' => 'result_disabled_4']
11340
        );
11341
11342
        $resultDisabledGroup[] = $form->createElement(
11343
            'radio',
11344
            'results_disabled',
11345
            null,
11346
            get_lang(
11347
                'Do not show the score (only when user finishes all attempts) but show feedback for each attempt.'
11348
            ),
11349
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
11350
            [
11351
                'id' => 'result_disabled_5',
11352
                //'onclick' => 'check_results_disabled()'
11353
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11354
            ]
11355
        );
11356
11357
        $resultDisabledGroup[] = $form->createElement(
11358
            'radio',
11359
            'results_disabled',
11360
            null,
11361
            get_lang(
11362
                'Ranking mode: Do not show results details question by question and show a table with the ranking of all other users.'
11363
            ),
11364
            RESULT_DISABLE_RANKING,
11365
            ['id' => 'result_disabled_6']
11366
        );
11367
11368
        $resultDisabledGroup[] = $form->createElement(
11369
            'radio',
11370
            'results_disabled',
11371
            null,
11372
            get_lang(
11373
                'Show only global score (not question score) and show only the correct answers, do not show incorrect answers at all'
11374
            ),
11375
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
11376
            ['id' => 'result_disabled_7']
11377
        );
11378
11379
        $resultDisabledGroup[] = $form->createElement(
11380
            'radio',
11381
            'results_disabled',
11382
            null,
11383
            get_lang('Auto-evaluation mode and ranking'),
11384
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
11385
            ['id' => 'result_disabled_8']
11386
        );
11387
11388
        $resultDisabledGroup[] = $form->createElement(
11389
            'radio',
11390
            'results_disabled',
11391
            null,
11392
            get_lang('Show score by category on a radar/spiderweb chart'),
11393
            RESULT_DISABLE_RADAR,
11394
            ['id' => 'result_disabled_9']
11395
        );
11396
11397
        $resultDisabledGroup[] = $form->createElement(
11398
            'radio',
11399
            'results_disabled',
11400
            null,
11401
            get_lang('Show the result to the learner: Show the score, the learner\'s choice and his feedback on each attempt, add the correct answer and his feedback when the chosen limit of attempts is reached.'),
11402
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
11403
            ['id' => 'result_disabled_10']
11404
        );
11405
11406
        return $form->addGroup(
11407
            $resultDisabledGroup,
11408
            null,
11409
            get_lang('Show score to learner')
11410
        );
11411
    }
11412
11413
    /**
11414
     * Return the text to display, based on the score and the max score.
11415
     * @param int|float $score
11416
     * @param int|float $maxScore
11417
     * @return string
11418
     */
11419
    public function getFinishText(int|float $score, int|float $maxScore): string
11420
    {
11421
        $passPercentage = $this->selectPassPercentage();
11422
        if (!empty($passPercentage)) {
11423
            $percentage = float_format(
11424
                ($score / (0 != $maxScore ? $maxScore : 1)) * 100,
11425
                1
11426
            );
11427
            if ($percentage >= $passPercentage) {
11428
                return $this->getTextWhenFinished();
11429
            } else {
11430
                return $this->getTextWhenFinishedFailure();
11431
            }
11432
        } else {
11433
            return $this->getTextWhenFinished();
11434
        }
11435
11436
        return '';
11437
    }
11438
}
11439