Passed
Pull Request — master (#6637)
by
unknown
14:37 queued 06:29
created

Exercise   F

Complexity

Total Complexity 1505

Size/Duplication

Total Lines 11342
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 6176
dl 0
loc 11342
rs 0.8
c 0
b 0
f 0
wmc 1505

186 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
B search_engine_edit() 0 73 9
A selectResultsDisabled() 0 3 1
F read() 0 109 22
D processCreation() 0 100 11
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 __construct() 0 52 2
A getFeedbackType() 0 3 1
A hasQuestion() 0 17 1
A setScoreTypeModel() 0 3 1
A selectTimeLimit() 0 3 1
B setResultFeedbackGroup() 0 93 6
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
B cleanResults() 0 70 7
B delete() 0 72 9
A selectTitle() 0 7 2
A updateStatus() 0 3 1
A getGlobalCategoryId() 0 3 1
A updateRandomByCat() 0 12 2
A selectPropagateNeg() 0 3 1
B copyExercise() 0 53 11
A getRandomAnswers() 0 3 1
A selectNbrQuestions() 0 3 1
B search_engine_save() 0 52 8
A getScoreTypeModel() 0 3 1
A selectDisplayCategoryName() 0 3 1
A selectType() 0 3 1
A getQuestionOrderedListByName() 0 24 4
A selectExpiredTime() 0 3 1
A selectStatus() 0 3 1
A getShuffle() 0 3 1
A setTextWhenFinishedFailure() 0 3 1
F createForm() 0 652 40
F save() 0 132 21
A getSaveCorrectAnswers() 0 3 1
A getTextWhenFinished() 0 3 1
B search_engine_delete() 0 38 6
A getQuestionCount() 0 19 2
A get_stat_track_exercise_info() 0 31 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
A save_stat_track_exercise_info() 0 51 3
A showTimeControlJS() 0 65 4
F show_button() 0 177 37
A showSimpleTimeControl() 0 42 1
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 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 2986 507
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
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 771 122
A getFinishText() 0 18 4
C editQuestionToRemind() 0 52 13
A setPageResultConfiguration() 0 12 2
B getReminderTable() 0 142 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 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 22 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\TrackEExercise;
7
use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
8
use Chamilo\CoreBundle\Entity\TrackEHotspot;
9
use Chamilo\CoreBundle\Enums\ActionIcon;
10
use Chamilo\CoreBundle\Enums\StateIcon;
11
use Chamilo\CoreBundle\Enums\ToolIcon;
12
use Chamilo\CoreBundle\Framework\Container;
13
use Chamilo\CoreBundle\Repository\ResourceLinkRepository;
14
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
15
use Chamilo\CoreBundle\Helpers\ChamiloHelper;
16
use Chamilo\CourseBundle\Entity\CQuiz;
17
use Chamilo\CourseBundle\Entity\CQuizCategory;
18
use Chamilo\CourseBundle\Entity\CQuizRelQuestionCategory;
19
use ChamiloSession as Session;
20
21
22
/**
23
 * @author Olivier Brouckaert
24
 * @author Julio Montoya Cleaning exercises
25
 * @author Hubert Borderiou #294
26
 */
27
class Exercise
28
{
29
    public const PAGINATION_ITEMS_PER_PAGE = 20;
30
    public $iId;
31
    public $id;
32
    public $name;
33
    public $title;
34
    public $exercise;
35
    public $description;
36
    public $sound;
37
    public $type; //ALL_ON_ONE_PAGE or ONE_PER_PAGE
38
    public $random;
39
    public $random_answers;
40
    public $active;
41
    public $timeLimit;
42
    public $attempts;
43
    public $feedback_type;
44
    public $end_time;
45
    public $start_time;
46
    public $questionList; // array with the list of this exercise's questions
47
    /* including question list of the media */
48
    public $questionListUncompressed;
49
    public $results_disabled;
50
    public $expired_time;
51
    public $course;
52
    public $course_id;
53
    public $propagate_neg;
54
    public $saveCorrectAnswers;
55
    public $review_answers;
56
    public $randomByCat;
57
    public $text_when_finished;
58
    public $text_when_finished_failure;
59
    public $display_category_name;
60
    public $pass_percentage;
61
    public $edit_exercise_in_lp = false;
62
    public $is_gradebook_locked = false;
63
    public $exercise_was_added_in_lp = false;
64
    public $lpList = [];
65
    public $force_edit_exercise_in_lp = false;
66
    public $categories;
67
    public $categories_grouping = true;
68
    public $endButton = 0;
69
    public $categoryWithQuestionList;
70
    public $mediaList;
71
    public $loadQuestionAJAX = false;
72
    // Notification send to the teacher.
73
    public $emailNotificationTemplate = null;
74
    // Notification send to the student.
75
    public $emailNotificationTemplateToUser = null;
76
    public $countQuestions = 0;
77
    public $fastEdition = false;
78
    public $modelType = 1;
79
    public $questionSelectionType = EX_Q_SELECTION_ORDERED;
80
    public $hideQuestionTitle = 0;
81
    public $scoreTypeModel = 0;
82
    public $categoryMinusOne = true; // Shows the category -1: See BT#6540
83
    public $globalCategoryId = null;
84
    public $onSuccessMessage = null;
85
    public $onFailedMessage = null;
86
    public $emailAlert;
87
    public $notifyUserByEmail = '';
88
    public $sessionId = 0;
89
    public $questionFeedbackEnabled = false;
90
    public $questionTypeWithFeedback;
91
    public $showPreviousButton;
92
    public $notifications;
93
    public $export = false;
94
    public $autolaunch;
95
    public $quizCategoryId;
96
    public $pageResultConfiguration;
97
    public $hideQuestionNumber;
98
    public $preventBackwards;
99
    public $currentQuestion;
100
    public $hideComment;
101
    public $hideNoAnswer;
102
    public $hideExpectedAnswer;
103
    public $forceShowExpectedChoiceColumn;
104
    public $disableHideCorrectAnsweredQuestions;
105
106
    /**
107
     * @param int $courseId
108
     */
109
    public function __construct($courseId = 0)
110
    {
111
        $this->iId = 0;
112
        $this->id = 0;
113
        $this->exercise = '';
114
        $this->description = '';
115
        $this->sound = '';
116
        $this->type = ALL_ON_ONE_PAGE;
117
        $this->random = 0;
118
        $this->random_answers = 0;
119
        $this->active = 1;
120
        $this->questionList = [];
121
        $this->timeLimit = 0;
122
        $this->end_time = '';
123
        $this->start_time = '';
124
        $this->results_disabled = 1;
125
        $this->expired_time = 0;
126
        $this->propagate_neg = 0;
127
        $this->saveCorrectAnswers = 0;
128
        $this->review_answers = false;
129
        $this->randomByCat = 0;
130
        $this->text_when_finished = '';
131
        $this->text_when_finished_failure = '';
132
        $this->display_category_name = 0;
133
        $this->pass_percentage = 0;
134
        $this->modelType = 1;
135
        $this->questionSelectionType = EX_Q_SELECTION_ORDERED;
136
        $this->endButton = 0;
137
        $this->scoreTypeModel = 0;
138
        $this->globalCategoryId = null;
139
        $this->notifications = [];
140
        $this->quizCategoryId = 0;
141
        $this->pageResultConfiguration = null;
142
        $this->hideQuestionNumber = 0;
143
        $this->preventBackwards = 0;
144
        $this->hideComment = false;
145
        $this->hideNoAnswer = false;
146
        $this->hideExpectedAnswer = false;
147
        $this->disableHideCorrectAnsweredQuestions = false;
148
149
        if (!empty($courseId)) {
150
            $courseInfo = api_get_course_info_by_id($courseId);
151
        } else {
152
            $courseInfo = api_get_course_info();
153
        }
154
        $this->course_id = $courseInfo['real_id'];
155
        $this->course = $courseInfo;
156
        $this->sessionId = api_get_session_id();
157
158
        // ALTER TABLE c_quiz_question ADD COLUMN feedback text;
159
        $this->questionFeedbackEnabled = ('true' === api_get_setting('exercise.allow_quiz_question_feedback'));
160
        $this->showPreviousButton = true;
161
    }
162
163
    /**
164
     * Reads exercise information from the data base.
165
     *
166
     * @param int  $id                - exercise Id
167
     * @param bool $parseQuestionList
168
     *
169
     * @return bool - true if exercise exists, otherwise false
170
     */
171
    public function read($id, $parseQuestionList = true)
172
    {
173
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
174
175
        $id = (int) $id;
176
        if (empty($this->course_id) || empty($id)) {
177
            return false;
178
        }
179
180
        $sql = "SELECT * FROM $table
181
                WHERE iid = $id";
182
        $result = Database::query($sql);
183
184
        // if the exercise has been found
185
        if ($object = Database::fetch_object($result)) {
186
            $this->id = $this->iId = (int) $object->iid;
187
            $this->exercise = $object->title;
188
            $this->name = $object->title;
189
            $this->title = $object->title;
190
            $this->description = $object->description;
191
            $this->sound = $object->sound;
192
            $this->type = $object->type;
193
            if (empty($this->type)) {
194
                $this->type = ONE_PER_PAGE;
195
            }
196
            $this->random = $object->random;
197
            $this->random_answers = $object->random_answers;
198
            $this->active = $object->active;
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
        $active = $this->active;
1579
        $propagate_neg = (int) $this->propagate_neg;
1580
        $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
1581
        $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
1582
        $randomByCat = (int) $this->randomByCat;
1583
        $text_when_finished = $this->text_when_finished;
1584
        $text_when_finished_failure = $this->text_when_finished_failure;
1585
        $display_category_name = (int) $this->display_category_name;
1586
        $pass_percentage = (int) $this->pass_percentage;
1587
1588
        // If direct we do not show results
1589
        $results_disabled = (int) $this->results_disabled;
1590
        if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1591
            $results_disabled = 0;
1592
        }
1593
        $expired_time = (int) $this->expired_time;
1594
1595
        $repo = Container::getQuizRepository();
1596
        $repoCategory = Container::getQuizCategoryRepository();
1597
1598
        // we prepare date in the database using the api_get_utc_datetime() function
1599
        $start_time = null;
1600
        if (!empty($this->start_time)) {
1601
            $start_time = $this->start_time;
1602
        }
1603
1604
        $end_time = null;
1605
        if (!empty($this->end_time)) {
1606
            $end_time = $this->end_time;
1607
        }
1608
1609
        // Exercise already exists
1610
        if ($id) {
1611
            /** @var CQuiz $exercise */
1612
            $exercise = $repo->find($id);
1613
        } else {
1614
            $exercise = new CQuiz();
1615
        }
1616
1617
        $exercise
1618
            ->setStartTime($start_time)
1619
            ->setEndTime($end_time)
1620
            ->setTitle($title)
1621
            ->setDescription($description)
1622
            ->setSound($sound)
1623
            ->setType($type)
1624
            ->setRandom((int) $random)
1625
            ->setRandomAnswers((bool) $random_answers)
1626
            ->setActive((int) $active)
1627
            ->setResultsDisabled($results_disabled)
1628
            ->setMaxAttempt($attempts)
1629
            ->setFeedbackType($feedback_type)
1630
            ->setExpiredTime($expired_time)
1631
            ->setReviewAnswers($review_answers)
1632
            ->setRandomByCategory($randomByCat)
1633
            ->setTextWhenFinished($text_when_finished)
1634
            ->setTextWhenFinishedFailure($text_when_finished_failure)
1635
            ->setDisplayCategoryName($display_category_name)
1636
            ->setPassPercentage($pass_percentage)
1637
            ->setSaveCorrectAnswers($saveCorrectAnswers)
1638
            ->setPropagateNeg($propagate_neg)
1639
            ->setHideQuestionTitle(1 === (int) $this->getHideQuestionTitle())
1640
            ->setQuestionSelectionType($this->getQuestionSelectionType())
1641
            ->setHideQuestionNumber((int) $this->hideQuestionNumber)
1642
        ;
1643
1644
        $allow = ('true' === api_get_setting('exercise.allow_exercise_categories'));
1645
        if (true === $allow && !empty($this->getQuizCategoryId())) {
1646
            $exercise->setQuizCategory($repoCategory->find($this->getQuizCategoryId()));
1647
        }
1648
1649
        $exercise->setPreventBackwards($this->getPreventBackwards());
1650
1651
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
1652
        if (true === $allow) {
1653
            $exercise->setShowPreviousButton($this->showPreviousButton());
1654
        }
1655
1656
        $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
1657
        if (true === $allow) {
1658
            $notifications = $this->getNotifications();
1659
            if (!empty($notifications)) {
1660
                $notifications = implode(',', $notifications);
1661
                $exercise->setNotifications($notifications);
1662
            }
1663
        }
1664
1665
        if (!empty($this->pageResultConfiguration)) {
1666
            $exercise->setPageResultConfiguration($this->pageResultConfiguration);
1667
        }
1668
1669
        $em = Database::getManager();
1670
1671
        if ($id) {
1672
            $repo->updateNodeForResource($exercise);
1673
1674
            if ('true' === api_get_setting('search_enabled')) {
1675
                $this->search_engine_edit();
1676
            }
1677
            $em->persist($exercise);
1678
            $em->flush();
1679
        } else {
1680
            // Creates a new exercise
1681
            $courseEntity = api_get_course_entity($this->course_id);
1682
            $exercise
1683
                ->setParent($courseEntity)
1684
                ->addCourseLink($courseEntity, api_get_session_entity());
1685
            $em->persist($exercise);
1686
            $em->flush();
1687
            $id = $exercise->getIid();
1688
            $this->iId = $this->id = $id;
1689
            if ($id) {
1690
                if ('true' === api_get_setting('search_enabled') && extension_loaded('xapian')) {
1691
                    $this->search_engine_save();
1692
                }
1693
            }
1694
        }
1695
1696
        $this->saveCategoriesInExercise($this->categories);
1697
1698
        return $id;
1699
    }
1700
1701
    /**
1702
     * Updates question position.
1703
     *
1704
     * @return bool
1705
     */
1706
    public function update_question_positions()
1707
    {
1708
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1709
        // Fixes #3483 when updating order
1710
        $questionList = $this->selectQuestionList(true);
1711
1712
        if (empty($this->getId())) {
1713
            return false;
1714
        }
1715
1716
        if (!empty($questionList)) {
1717
            foreach ($questionList as $position => $questionId) {
1718
                $position = (int) $position;
1719
                $questionId = (int) $questionId;
1720
                $sql = "UPDATE $table SET
1721
                            question_order = $position
1722
                        WHERE
1723
                            question_id = $questionId AND
1724
                            quiz_id= ".$this->getId();
1725
                Database::query($sql);
1726
            }
1727
        }
1728
1729
        return true;
1730
    }
1731
1732
    /**
1733
     * Adds a question into the question list.
1734
     *
1735
     * @param int $questionId - question ID
1736
     *
1737
     * @return bool - true if the question has been added, otherwise false
1738
     *
1739
     * @author Olivier Brouckaert
1740
     */
1741
    public function addToList($questionId)
1742
    {
1743
        // checks if the question ID is not in the list
1744
        if (!$this->isInList($questionId)) {
1745
            // selects the max position
1746
            if (!$this->selectNbrQuestions()) {
1747
                $pos = 1;
1748
            } else {
1749
                if (is_array($this->questionList)) {
1750
                    $pos = max(array_keys($this->questionList)) + 1;
1751
                }
1752
            }
1753
            $this->questionList[$pos] = $questionId;
1754
1755
            return true;
1756
        }
1757
1758
        return false;
1759
    }
1760
1761
    /**
1762
     * removes a question from the question list.
1763
     *
1764
     * @param int $questionId - question ID
1765
     *
1766
     * @return bool - true if the question has been removed, otherwise false
1767
     *
1768
     * @author Olivier Brouckaert
1769
     */
1770
    public function removeFromList($questionId)
1771
    {
1772
        // searches the position of the question ID in the list
1773
        $pos = array_search($questionId, $this->questionList);
1774
        // question not found
1775
        if (false === $pos) {
1776
            return false;
1777
        } else {
1778
            // dont reduce the number of random question if we use random by category option, or if
1779
            // random all questions
1780
            if ($this->isRandom() && 0 == $this->isRandomByCat()) {
1781
                if (count($this->questionList) >= $this->random && $this->random > 0) {
1782
                    $this->random--;
1783
                    $this->save();
1784
                }
1785
            }
1786
            // deletes the position from the array containing the wanted question ID
1787
            unset($this->questionList[$pos]);
1788
1789
            return true;
1790
        }
1791
    }
1792
1793
    /**
1794
     * deletes the exercise from the database
1795
     * Notice : leaves the question in the data base.
1796
     *
1797
     * @author Olivier Brouckaert
1798
     */
1799
    public function delete()
1800
    {
1801
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
1802
1803
        if ($limitTeacherAccess && !api_is_platform_admin()) {
1804
            return false;
1805
        }
1806
1807
        $exerciseId = $this->iId;
1808
1809
        $repo = Container::getQuizRepository();
1810
        /** @var CQuiz $exercise */
1811
        $exercise = $repo->find($exerciseId);
1812
        $linksRepo = Container::$container->get(ResourceLinkRepository::class);
1813
1814
        if (null === $exercise) {
1815
            return false;
1816
        }
1817
1818
        $locked = api_resource_is_locked_by_gradebook(
1819
            $exerciseId,
1820
            LINK_EXERCISE
1821
        );
1822
1823
        if ($locked) {
1824
            return false;
1825
        }
1826
1827
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
1828
        $sql = "UPDATE $table SET active='-1'
1829
                WHERE iid = $exerciseId";
1830
        Database::query($sql);
1831
1832
        $course = api_get_course_entity();
1833
        $session = api_get_session_entity();
1834
1835
        $linksRepo->removeByResourceInContext($exercise, $course, $session);
1836
1837
        SkillModel::deleteSkillsFromItem($exerciseId, ITEM_TYPE_EXERCISE);
1838
1839
        if ('true' === api_get_setting('search_enabled') &&
1840
            extension_loaded('xapian')
1841
        ) {
1842
            $this->search_engine_delete();
1843
        }
1844
1845
        $linkInfo = GradebookUtils::isResourceInCourseGradebook(
1846
            $this->course_id,
1847
            LINK_EXERCISE,
1848
            $exerciseId,
1849
            $this->sessionId
1850
        );
1851
        if (!empty($linkInfo)) {
1852
            GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
1853
        }
1854
1855
        // Register resource deletion manually because this is a soft delete (active = -1)
1856
        // and Doctrine does not trigger postRemove in this case.
1857
        /* @var TrackEDefaultRepository $trackRepo */
1858
        $trackRepo = Container::$container->get(TrackEDefaultRepository::class);
1859
        $resourceNode = $exercise->getResourceNode();
1860
        if ($resourceNode) {
1861
            $trackRepo->registerResourceEvent(
1862
                $resourceNode,
1863
                'deletion',
1864
                api_get_user_id(),
1865
                api_get_course_int_id(),
1866
                api_get_session_id()
1867
            );
1868
        }
1869
1870
        return true;
1871
    }
1872
1873
    /**
1874
     * Creates the form to create / edit an exercise.
1875
     *
1876
     * @param FormValidator $form
1877
     * @param string|array        $type
1878
     */
1879
    public function createForm($form, $type = 'full')
1880
    {
1881
        if (empty($type)) {
1882
            $type = 'full';
1883
        }
1884
1885
        // Form title
1886
        $form_title = get_lang('Create a new test');
1887
        if (!empty($_GET['id'])) {
1888
            $form_title = get_lang('Edit test name and settings');
1889
        }
1890
1891
        $form->addHeader($form_title);
1892
1893
        // Title.
1894
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
1895
            $form->addHtmlEditor(
1896
                'exerciseTitle',
1897
                get_lang('Test name'),
1898
                false,
1899
                false,
1900
                ['ToolbarSet' => 'TitleAsHtml']
1901
            );
1902
        } else {
1903
            $form->addElement(
1904
                'text',
1905
                'exerciseTitle',
1906
                get_lang('Test name'),
1907
                ['id' => 'exercise_title']
1908
            );
1909
        }
1910
1911
        $form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings'));
1912
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1913
1914
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
1915
            $categoryManager = new ExerciseCategoryManager();
1916
            $categories = $categoryManager->getCategories(api_get_course_int_id());
1917
            $options = [];
1918
            if (!empty($categories)) {
1919
                /** @var CQuizCategory $category */
1920
                foreach ($categories as $category) {
1921
                    $options[$category->getId()] = $category->getTitle();
1922
                }
1923
            }
1924
1925
            $form->addSelect(
1926
                'quiz_category_id',
1927
                get_lang('Category'),
1928
                $options,
1929
                ['placeholder' => get_lang('Please select an option')]
1930
            );
1931
        }
1932
1933
        $editor_config = [
1934
            'ToolbarSet' => 'TestQuestionDescription',
1935
            'Width' => '100%',
1936
            'Height' => '150',
1937
        ];
1938
1939
        if (is_array($type)) {
1940
            $editor_config = array_merge($editor_config, $type);
1941
        }
1942
1943
        $form->addHtmlEditor(
1944
            'exerciseDescription',
1945
            get_lang('Give a context to the test'),
1946
            false,
1947
            false,
1948
            $editor_config
1949
        );
1950
1951
        $skillList = [];
1952
        if ('full' === $type) {
1953
            // Can't modify a DirectFeedback question.
1954
            if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1955
                $this->setResultFeedbackGroup($form);
1956
1957
                // Type of results display on the final page
1958
                $this->setResultDisabledGroup($form);
1959
1960
                // Type of questions disposition on page
1961
                $radios = [];
1962
                $radios[] = $form->createElement(
1963
                    'radio',
1964
                    'exerciseType',
1965
                    null,
1966
                    get_lang('All questions on one page'),
1967
                    '1',
1968
                    [
1969
                        'onclick' => 'check_per_page_all()',
1970
                        'id' => 'option_page_all',
1971
                    ]
1972
                );
1973
                $radios[] = $form->createElement(
1974
                    'radio',
1975
                    'exerciseType',
1976
                    null,
1977
                    get_lang('One question by page'),
1978
                    '2',
1979
                    [
1980
                        'onclick' => 'check_per_page_one()',
1981
                        'id' => 'option_page_one',
1982
                    ]
1983
                );
1984
1985
                $form->addGroup($radios, null, get_lang('Questions per page'));
1986
            } else {
1987
                // if is Direct feedback but has not questions we can allow to modify the question type
1988
                if (empty($this->iId) || 0 === $this->getQuestionCount()) {
1989
                    $this->setResultFeedbackGroup($form);
1990
                    $this->setResultDisabledGroup($form);
1991
1992
                    // Type of questions disposition on page
1993
                    $radios = [];
1994
                    $radios[] = $form->createElement(
1995
                        'radio',
1996
                        'exerciseType',
1997
                        null,
1998
                        get_lang('All questions on one page'),
1999
                        '1'
2000
                    );
2001
                    $radios[] = $form->createElement(
2002
                        'radio',
2003
                        'exerciseType',
2004
                        null,
2005
                        get_lang('One question by page'),
2006
                        '2'
2007
                    );
2008
                    $form->addGroup($radios, null, get_lang('Sequential'));
2009
                } else {
2010
                    $this->setResultFeedbackGroup($form, true);
2011
                    $group = $this->setResultDisabledGroup($form);
2012
                    $group->freeze();
2013
2014
                    // we force the options to the DirectFeedback exercisetype
2015
                    //$form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
2016
                    //$form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
2017
2018
                    // Type of questions disposition on page
2019
                    $radios[] = $form->createElement(
2020
                        'radio',
2021
                        'exerciseType',
2022
                        null,
2023
                        get_lang('All questions on one page'),
2024
                        '1',
2025
                        [
2026
                            'onclick' => 'check_per_page_all()',
2027
                            'id' => 'option_page_all',
2028
                        ]
2029
                    );
2030
                    $radios[] = $form->createElement(
2031
                        'radio',
2032
                        'exerciseType',
2033
                        null,
2034
                        get_lang('One question by page'),
2035
                        '2',
2036
                        [
2037
                            'onclick' => 'check_per_page_one()',
2038
                            'id' => 'option_page_one',
2039
                        ]
2040
                    );
2041
2042
                    $type_group = $form->addGroup($radios, null, get_lang('Questions per page'));
2043
                    $type_group->freeze();
2044
                }
2045
            }
2046
2047
            $option = [
2048
                EX_Q_SELECTION_ORDERED => get_lang('Ordered by user'),
2049
                //  Defined by user
2050
                EX_Q_SELECTION_RANDOM => get_lang('Random'),
2051
                // 1-10, All
2052
                'per_categories' => '--------'.get_lang('Using categories').'----------',
2053
                // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
2054
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang(
2055
                    'Ordered categories alphabetically with questions ordered'
2056
                ),
2057
                // A 123 B 456 C 78 (0, 1, all)
2058
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang(
2059
                    'Random categories with questions ordered'
2060
                ),
2061
                // C 78 B 456 A 123
2062
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang(
2063
                    'Ordered categories alphabetically with random questions'
2064
                ),
2065
                // A 321 B 654 C 87
2066
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang(
2067
                    'Random categories with random questions'
2068
                ),
2069
                // C 87 B 654 A 321
2070
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('Random categories with questions ordered (questions not grouped)'),
2071
                /*    B 456 C 78 A 123
2072
                        456 78 123
2073
                        123 456 78
2074
                */
2075
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('Random categories with random questions (questions not grouped)'),
2076
                /*
2077
                    A 123 B 456 C 78
2078
                    B 456 C 78 A 123
2079
                    B 654 C 87 A 321
2080
                    654 87 321
2081
                    165 842 73
2082
                */
2083
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('Ordered categories by parent with questions ordered'),
2084
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('Ordered categories by parent with random questions'),
2085
            ];
2086
2087
            $form->addSelect(
2088
                'question_selection_type',
2089
                [get_lang('Question selection type')],
2090
                $option,
2091
                [
2092
                    'id' => 'questionSelection',
2093
                    'onchange' => 'checkQuestionSelection()',
2094
                ]
2095
            );
2096
2097
            $group = [
2098
                $form->createElement(
2099
                    'checkbox',
2100
                    'hide_expected_answer',
2101
                    null,
2102
                    get_lang('Hide expected answers column')
2103
                ),
2104
                $form->createElement(
2105
                    'checkbox',
2106
                    'hide_total_score',
2107
                    null,
2108
                    get_lang('Hide total score')
2109
                ),
2110
                $form->createElement(
2111
                    'checkbox',
2112
                    'hide_question_score',
2113
                    null,
2114
                    get_lang('Hide question score')
2115
                ),
2116
                $form->createElement(
2117
                    'checkbox',
2118
                    'hide_category_table',
2119
                    null,
2120
                    get_lang('Hide category table')
2121
                ),
2122
                $form->createElement(
2123
                    'checkbox',
2124
                    'hide_correct_answered_questions',
2125
                    null,
2126
                    get_lang('Hide correct answered questions')
2127
                ),
2128
            ];
2129
            $form->addGroup($group, null, get_lang('Results page configuration'));
2130
2131
            $group = [
2132
                $form->createElement('radio', 'hide_question_number', null, get_lang('Yes'), '1'),
2133
                $form->createElement('radio', 'hide_question_number', null, get_lang('No'), '0'),
2134
            ];
2135
            $form->addGroup($group, null, get_lang('Hide question numbering'));
2136
2137
            $displayMatrix = 'none';
2138
            $displayRandom = 'none';
2139
            $selectionType = $this->getQuestionSelectionType();
2140
            switch ($selectionType) {
2141
                case EX_Q_SELECTION_RANDOM:
2142
                    $displayRandom = 'block';
2143
2144
                    break;
2145
                case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
2146
                    $displayMatrix = 'block';
2147
2148
                    break;
2149
            }
2150
2151
            $form->addHtml('<div id="hidden_random" style="display:'.$displayRandom.'">');
2152
            // Number of random question.
2153
            $max = $this->getId() > 0 ? $this->getQuestionCount() : 10;
2154
            $option = range(0, $max);
2155
            $option[0] = get_lang('No');
2156
            $option[-1] = get_lang('All');
2157
            $form->addSelect(
2158
                'randomQuestions',
2159
                [
2160
                    get_lang('Random questions'),
2161
                    get_lang("To randomize all questions choose 10. To disable randomization, choose \"Do not randomize\"."),
2162
                ],
2163
                $option,
2164
                ['id' => 'randomQuestions']
2165
            );
2166
            $form->addHtml('</div>');
2167
            $form->addHtml('<div id="hidden_matrix" style="display:'.$displayMatrix.'">');
2168
2169
            // Category selection.
2170
            $cat = new TestCategory();
2171
            $cat_form = $cat->returnCategoryForm($this);
2172
            if (empty($cat_form)) {
2173
                $cat_form = '<span class="label label-warning">'.get_lang('No categories defined').'</span>';
2174
            }
2175
            $form->addElement('label', null, $cat_form);
2176
            $form->addHtml('</div>');
2177
2178
            // Random answers.
2179
            $radios_random_answers = [
2180
                $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
2181
                $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
2182
            ];
2183
            $form->addGroup($radios_random_answers, null, get_lang('Shuffle answers'));
2184
2185
            // Category name.
2186
            $radio_display_cat_name = [
2187
                $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
2188
                $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
2189
            ];
2190
            $form->addGroup($radio_display_cat_name, null, get_lang('Display questions category'));
2191
2192
            // Hide question title.
2193
            $group = [
2194
                $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
2195
                $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
2196
            ];
2197
            $form->addGroup($group, null, get_lang('Hide question title'));
2198
2199
            $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
2200
2201
            if (true === $allow) {
2202
                // Hide question title.
2203
                $group = [
2204
                    $form->createElement(
2205
                        'radio',
2206
                        'show_previous_button',
2207
                        null,
2208
                        get_lang('Yes'),
2209
                        '1'
2210
                    ),
2211
                    $form->createElement(
2212
                        'radio',
2213
                        'show_previous_button',
2214
                        null,
2215
                        get_lang('No'),
2216
                        '0'
2217
                    ),
2218
                ];
2219
                $form->addGroup($group, null, get_lang('Show previous button'));
2220
            }
2221
2222
            $form->addElement(
2223
                'number',
2224
                'exerciseAttempts',
2225
                get_lang('Max number of attempts'),
2226
                null,
2227
                ['id' => 'exerciseAttempts']
2228
            );
2229
2230
            // Exercise time limit
2231
            $form->addElement(
2232
                'checkbox',
2233
                'activate_start_date_check',
2234
                null,
2235
                get_lang('Enable start time'),
2236
                ['onclick' => 'activate_start_date()']
2237
            );
2238
2239
            if (!empty($this->start_time)) {
2240
                $form->addElement('html', '<div id="start_date_div" style="display:block;">');
2241
            } else {
2242
                $form->addElement('html', '<div id="start_date_div" style="display:none;">');
2243
            }
2244
2245
            $form->addElement('date_time_picker', 'start_time');
2246
            $form->addElement('html', '</div>');
2247
            $form->addElement(
2248
                'checkbox',
2249
                'activate_end_date_check',
2250
                null,
2251
                get_lang('Enable end time'),
2252
                ['onclick' => 'activate_end_date()']
2253
            );
2254
2255
            if (!empty($this->end_time)) {
2256
                $form->addHtml('<div id="end_date_div" style="display:block;">');
2257
            } else {
2258
                $form->addHtml('<div id="end_date_div" style="display:none;">');
2259
            }
2260
2261
            $form->addElement('date_time_picker', 'end_time');
2262
            $form->addElement('html', '</div>');
2263
2264
            $display = 'block';
2265
            $form->addElement(
2266
                'checkbox',
2267
                'propagate_neg',
2268
                null,
2269
                get_lang('Propagate negative results between questions')
2270
            );
2271
2272
            $options = [
2273
                '' => get_lang('Please select an option'),
2274
                1 => get_lang('Save the correct answer for the next attempt'),
2275
                2 => get_lang('Pre-fill with answers from previous attempt'),
2276
            ];
2277
            $form->addSelect(
2278
                'save_correct_answers',
2279
                get_lang('Save answers'),
2280
                $options
2281
            );
2282
2283
            $form->addElement('html', '<div class="clear">&nbsp;</div>');
2284
            $form->addCheckBox('review_answers', null, get_lang('Review my answers'));
2285
            $form->addElement('html', '<div id="divtimecontrol"  style="display:'.$display.';">');
2286
2287
            // Timer control
2288
            $form->addElement(
2289
                'checkbox',
2290
                'enabletimercontrol',
2291
                null,
2292
                get_lang('Enable time control'),
2293
                [
2294
                    'onclick' => 'option_time_expired()',
2295
                    'id' => 'enabletimercontrol',
2296
                    'onload' => 'check_load_time()',
2297
                ]
2298
            );
2299
2300
            $expired_date = (int) $this->selectExpiredTime();
2301
2302
            if (('0' != $expired_date)) {
2303
                $form->addElement('html', '<div id="timercontrol" style="display:block;">');
2304
            } else {
2305
                $form->addElement('html', '<div id="timercontrol" style="display:none;">');
2306
            }
2307
            $form->addText(
2308
                'enabletimercontroltotalminutes',
2309
                get_lang('Total duration in minutes of the test'),
2310
                false,
2311
                [
2312
                    'id' => 'enabletimercontroltotalminutes',
2313
                    'cols-size' => [2, 2, 8],
2314
                ]
2315
            );
2316
            $form->addElement('html', '</div>');
2317
            $form->addCheckBox('prevent_backwards', null, get_lang('Prevent moving backwards between questions'));
2318
            $form->addElement(
2319
                'text',
2320
                'pass_percentage',
2321
                [get_lang('Pass percentage'), null, '%'],
2322
                ['id' => 'pass_percentage']
2323
            );
2324
2325
            $form->addRule('pass_percentage', get_lang('Numerical'), 'numeric');
2326
            $form->addRule('pass_percentage', get_lang('Value is too small.'), 'min_numeric_length', 0);
2327
            $form->addRule('pass_percentage', get_lang('Value is too big.'), 'max_numeric_length', 100);
2328
2329
            // add the text_when_finished textbox
2330
            $form->addHtmlEditor(
2331
                'text_when_finished',
2332
                get_lang('Text appearing at the end of the test when the user has succeeded or if no pass percentage was set.'),
2333
                false,
2334
                false,
2335
                $editor_config
2336
            );
2337
            $form->addHtmlEditor(
2338
                'text_when_finished_failure',
2339
                get_lang('Text appearing at the end of the test when the user has failed.'),
2340
                false,
2341
                false,
2342
                $editor_config
2343
            );
2344
2345
            $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
2346
            if (true === $allow) {
2347
                $settings = ExerciseLib::getNotificationSettings();
2348
                $group = [];
2349
                foreach ($settings as $itemId => $label) {
2350
                    $group[] = $form->createElement(
2351
                        'checkbox',
2352
                        'notifications[]',
2353
                        null,
2354
                        $label,
2355
                        ['value' => $itemId]
2356
                    );
2357
                }
2358
                $form->addGroup($group, '', [get_lang('E-mail notifications')]);
2359
            }
2360
2361
            $form->addCheckBox('update_title_in_lps', null, get_lang('Update this title in learning paths'));
2362
2363
            $defaults = [];
2364
            if ('true' === api_get_setting('search_enabled')) {
2365
                $form->addCheckBox('index_document', '', get_lang('Index document text?'));
2366
                $form->addSelectLanguage('language', get_lang('Document language for indexation'));
2367
                $specific_fields = get_specific_field_list();
2368
2369
                foreach ($specific_fields as $specific_field) {
2370
                    $form->addElement('text', $specific_field['code'], $specific_field['name']);
2371
                    $filter = [
2372
                        'c_id' => api_get_course_int_id(),
2373
                        'field_id' => $specific_field['id'],
2374
                        'ref_id' => $this->getId(),
2375
                        'tool_id' => "'".TOOL_QUIZ."'",
2376
                    ];
2377
                    $values = get_specific_field_values_list($filter, ['value']);
2378
                    if (!empty($values)) {
2379
                        $arr_str_values = [];
2380
                        foreach ($values as $value) {
2381
                            $arr_str_values[] = $value['value'];
2382
                        }
2383
                        $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
2384
                    }
2385
                }
2386
            }
2387
2388
            $skillList = SkillModel::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
2389
2390
            $extraField = new ExtraField('exercise');
2391
            $extraField->addElements(
2392
                $form,
2393
                $this->iId,
2394
                ['notifications'], //exclude
2395
                false, // filter
2396
                false, // tag as select
2397
                [], //show only fields
2398
                [], // order fields
2399
                [] // extra data
2400
            );
2401
            $settings = api_get_configuration_value('exercise_finished_notification_settings');
2402
            if (!empty($settings)) {
2403
                $options = [];
2404
                foreach ($settings as $name => $data) {
2405
                    $options[$name] = $name;
2406
                }
2407
                $form->addSelect(
2408
                    'extra_notifications',
2409
                    get_lang('Notifications'),
2410
                    $options,
2411
                    ['placeholder' => get_lang('Please select an option')]
2412
                );
2413
            }
2414
            $form->addElement('html', '</div>'); //End advanced setting
2415
            $form->addElement('html', '</div>');
2416
        }
2417
2418
        // submit
2419
        if (isset($_GET['id'])) {
2420
            $form->addButtonSave(get_lang('Edit test name and settings'), 'submitExercise');
2421
        } else {
2422
            $form->addButtonUpdate(get_lang('Proceed to questions'), 'submitExercise');
2423
        }
2424
2425
        $form->addRule('exerciseTitle', get_lang('Name'), 'required');
2426
2427
        // defaults
2428
        if ('full' == $type) {
2429
            // rules
2430
            $form->addRule('exerciseAttempts', get_lang('Numerical'), 'numeric');
2431
            $form->addRule('start_time', get_lang('Invalid date'), 'datetime');
2432
            $form->addRule('end_time', get_lang('Invalid date'), 'datetime');
2433
2434
            if ($this->getId() > 0) {
2435
                $defaults['randomQuestions'] = $this->random;
2436
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2437
                $defaults['exerciseType'] = $this->selectType();
2438
                $defaults['exerciseTitle'] = $this->get_formated_title();
2439
                $defaults['exerciseDescription'] = $this->selectDescription();
2440
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2441
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2442
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2443
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2444
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2445
                $defaults['review_answers'] = $this->review_answers;
2446
                $defaults['randomByCat'] = $this->getRandomByCategory();
2447
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2448
                $defaults['text_when_finished_failure'] = $this->getTextWhenFinishedFailure();
2449
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2450
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2451
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2452
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2453
                $defaults['show_previous_button'] = $this->showPreviousButton();
2454
                $defaults['quiz_category_id'] = $this->getQuizCategoryId();
2455
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2456
                $defaults['hide_question_number'] = $this->getHideQuestionNumber();
2457
2458
                if (!empty($this->start_time)) {
2459
                    $defaults['activate_start_date_check'] = 1;
2460
                }
2461
                if (!empty($this->end_time)) {
2462
                    $defaults['activate_end_date_check'] = 1;
2463
                }
2464
2465
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date(
2466
                    'Y-m-d 12:00:00'
2467
                );
2468
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date(
2469
                    'Y-m-d 12:00:00',
2470
                    time() + 84600
2471
                );
2472
2473
                // Get expired time
2474
                if ('0' != $this->expired_time) {
2475
                    $defaults['enabletimercontrol'] = 1;
2476
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2477
                } else {
2478
                    $defaults['enabletimercontroltotalminutes'] = 0;
2479
                }
2480
                $defaults['skills'] = array_keys($skillList);
2481
                $defaults['notifications'] = $this->getNotifications();
2482
            } else {
2483
                $defaults['exerciseType'] = 2;
2484
                $defaults['exerciseAttempts'] = 0;
2485
                $defaults['randomQuestions'] = 0;
2486
                $defaults['randomAnswers'] = 0;
2487
                $defaults['exerciseDescription'] = '';
2488
                $defaults['exerciseFeedbackType'] = 0;
2489
                $defaults['results_disabled'] = 0;
2490
                $defaults['randomByCat'] = 0;
2491
                $defaults['text_when_finished'] = '';
2492
                $defaults['text_when_finished_failure'] = '';
2493
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2494
                $defaults['display_category_name'] = 1;
2495
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2496
                $defaults['pass_percentage'] = '';
2497
                $defaults['end_button'] = $this->selectEndButton();
2498
                $defaults['question_selection_type'] = 1;
2499
                $defaults['hide_question_title'] = 0;
2500
                $defaults['show_previous_button'] = 1;
2501
                $defaults['on_success_message'] = null;
2502
                $defaults['on_failed_message'] = null;
2503
            }
2504
        } else {
2505
            $defaults['exerciseTitle'] = $this->selectTitle();
2506
            $defaults['exerciseDescription'] = $this->selectDescription();
2507
        }
2508
2509
        if ('true' === api_get_setting('search_enabled')) {
2510
            $defaults['index_document'] = 'checked="checked"';
2511
        }
2512
2513
        $this->setPageResultConfigurationDefaults($defaults);
2514
        $form->setDefaults($defaults);
2515
2516
        // Freeze some elements.
2517
        if (0 != $this->getId() && false == $this->edit_exercise_in_lp) {
2518
            $elementsToFreeze = [
2519
                'randomQuestions',
2520
                //'randomByCat',
2521
                'exerciseAttempts',
2522
                'propagate_neg',
2523
                'enabletimercontrol',
2524
                'review_answers',
2525
            ];
2526
2527
            foreach ($elementsToFreeze as $elementName) {
2528
                /** @var HTML_QuickForm_element $element */
2529
                $element = $form->getElement($elementName);
2530
                $element->freeze();
2531
            }
2532
        }
2533
    }
2534
2535
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2536
    {
2537
        // Feedback type.
2538
        $feedback = [];
2539
        $warning = sprintf(
2540
            get_lang("The setting \"%s\" will change to \"%s\""),
2541
            get_lang('Show score to learner'),
2542
            get_lang('Auto-evaluation mode: show score and expected answers')
2543
        );
2544
        $endTest = $form->createElement(
2545
            'radio',
2546
            'exerciseFeedbackType',
2547
            null,
2548
            get_lang('At end of test'),
2549
            EXERCISE_FEEDBACK_TYPE_END,
2550
            [
2551
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2552
                //'onclick' => 'if confirm() check_feedback()',
2553
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_feedback(); } else { return false;} ',
2554
            ]
2555
        );
2556
2557
        $noFeedBack = $form->createElement(
2558
            'radio',
2559
            'exerciseFeedbackType',
2560
            null,
2561
            get_lang('Exam (no feedback)'),
2562
            EXERCISE_FEEDBACK_TYPE_EXAM,
2563
            [
2564
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2565
            ]
2566
        );
2567
2568
        $feedback[] = $endTest;
2569
        $feedback[] = $noFeedBack;
2570
2571
        $scenarioEnabled = 'true' === api_get_setting('enable_quiz_scenario');
2572
        $freeze = true;
2573
        if ($scenarioEnabled) {
2574
            if ($this->getQuestionCount() > 0) {
2575
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2576
                if (false === $hasDifferentQuestion) {
2577
                    $freeze = false;
2578
                }
2579
            } else {
2580
                $freeze = false;
2581
            }
2582
            // Can't convert a question from one feedback to another
2583
            $direct = $form->createElement(
2584
                'radio',
2585
                'exerciseFeedbackType',
2586
                null,
2587
                get_lang('Adaptative test with immediate feedback'),
2588
                EXERCISE_FEEDBACK_TYPE_DIRECT,
2589
                [
2590
                    'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2591
                    'onclick' => 'check_direct_feedback()',
2592
                ]
2593
            );
2594
2595
            $directPopUp = $form->createElement(
2596
                'radio',
2597
                'exerciseFeedbackType',
2598
                null,
2599
                get_lang('Direct pop-up mode'),
2600
                EXERCISE_FEEDBACK_TYPE_POPUP,
2601
                ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2602
            );
2603
            if ($freeze) {
2604
                $direct->freeze();
2605
                $directPopUp->freeze();
2606
            }
2607
2608
            // If has delineation freeze all.
2609
            $hasDelineation = $this->hasQuestionWithType(HOT_SPOT_DELINEATION);
2610
            if ($hasDelineation) {
2611
                $endTest->freeze();
2612
                $noFeedBack->freeze();
2613
                $direct->freeze();
2614
                $directPopUp->freeze();
2615
            }
2616
2617
            $feedback[] = $direct;
2618
            $feedback[] = $directPopUp;
2619
        }
2620
2621
        $form->addGroup(
2622
            $feedback,
2623
            null,
2624
            [
2625
                get_lang('Feedback'),
2626
                get_lang(
2627
                    '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.'
2628
                ),
2629
            ]
2630
        );
2631
    }
2632
2633
    /**
2634
     * function which process the creation of exercises.
2635
     *
2636
     * @param FormValidator $form
2637
     *
2638
     * @return int c_quiz.iid
2639
     */
2640
    public function processCreation($form)
2641
    {
2642
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2643
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2644
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2645
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2646
        $this->updateType($form->getSubmitValue('exerciseType'));
2647
2648
        // If direct feedback then force to One per page
2649
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2650
            $this->updateType(ONE_PER_PAGE);
2651
        }
2652
2653
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2654
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2655
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2656
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2657
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2658
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2659
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2660
        $this->setTextWhenFinished($form->getSubmitValue('text_when_finished'));
2661
        $this->setTextWhenFinishedFailure($form->getSubmitValue('text_when_finished_failure'));
2662
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2663
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2664
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2665
        $this->updateCategories($form->getSubmitValue('category'));
2666
        $this->updateEndButton($form->getSubmitValue('end_button'));
2667
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2668
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2669
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2670
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2671
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2672
        $this->setModelType($form->getSubmitValue('model_type'));
2673
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2674
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2675
        $this->sessionId = api_get_session_id();
2676
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2677
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2678
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2679
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2680
        $this->setNotifications($form->getSubmitValue('notifications'));
2681
        $this->setQuizCategoryId($form->getSubmitValue('quiz_category_id'));
2682
        $this->setPageResultConfiguration($form->getSubmitValues());
2683
        $this->setHideQuestionNumber($form->getSubmitValue('hide_question_number'));
2684
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
2685
2686
        $this->start_time = null;
2687
        if (1 == $form->getSubmitValue('activate_start_date_check')) {
2688
            $start_time = $form->getSubmitValue('start_time');
2689
            $this->start_time = api_get_utc_datetime($start_time);
2690
        }
2691
2692
        $this->end_time = null;
2693
        if (1 == $form->getSubmitValue('activate_end_date_check')) {
2694
            $end_time = $form->getSubmitValue('end_time');
2695
            $this->end_time = api_get_utc_datetime($end_time);
2696
        }
2697
2698
        $this->expired_time = 0;
2699
        if (1 == $form->getSubmitValue('enabletimercontrol')) {
2700
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
2701
            if (0 == $this->expired_time) {
2702
                $this->expired_time = $expired_total_time;
2703
            }
2704
        }
2705
2706
        $this->random_answers = 0;
2707
        if (1 == $form->getSubmitValue('randomAnswers')) {
2708
            $this->random_answers = 1;
2709
        }
2710
2711
        // Update title in all LPs that have this quiz added
2712
        if (1 == $form->getSubmitValue('update_title_in_lps')) {
2713
            $table = Database::get_course_table(TABLE_LP_ITEM);
2714
            $sql = "SELECT iid FROM $table
2715
                    WHERE
2716
                        item_type = 'quiz' AND
2717
                        path = '".$this->getId()."'
2718
                    ";
2719
            $result = Database::query($sql);
2720
            $items = Database::store_result($result);
2721
            if (!empty($items)) {
2722
                foreach ($items as $item) {
2723
                    $itemId = $item['iid'];
2724
                    $sql = "UPDATE $table
2725
                            SET title = '".$this->title."'
2726
                            WHERE iid = $itemId ";
2727
                    Database::query($sql);
2728
                }
2729
            }
2730
        }
2731
2732
        $iId = $this->save();
2733
        if (!empty($iId)) {
2734
            $values = $form->getSubmitValues();
2735
            $values['item_id'] = $iId;
2736
            $extraFieldValue = new ExtraFieldValue('exercise');
2737
            $extraFieldValue->saveFieldValues($values);
2738
2739
            SkillModel::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
2740
        }
2741
    }
2742
2743
    public function search_engine_save()
2744
    {
2745
        if (1 != $_POST['index_document']) {
2746
            return;
2747
        }
2748
        $course_id = api_get_course_id();
2749
        $specific_fields = get_specific_field_list();
2750
        $ic_slide = new IndexableChunk();
2751
2752
        $all_specific_terms = '';
2753
        foreach ($specific_fields as $specific_field) {
2754
            if (isset($_REQUEST[$specific_field['code']])) {
2755
                $sterms = trim($_REQUEST[$specific_field['code']]);
2756
                if (!empty($sterms)) {
2757
                    $all_specific_terms .= ' '.$sterms;
2758
                    $sterms = explode(',', $sterms);
2759
                    foreach ($sterms as $sterm) {
2760
                        $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2761
                        add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->getId(), $sterm);
2762
                    }
2763
                }
2764
            }
2765
        }
2766
2767
        // build the chunk to index
2768
        $ic_slide->addValue('title', $this->exercise);
2769
        $ic_slide->addCourseId($course_id);
2770
        $ic_slide->addToolId(TOOL_QUIZ);
2771
        $xapian_data = [
2772
            SE_COURSE_ID => $course_id,
2773
            SE_TOOL_ID => TOOL_QUIZ,
2774
            SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2775
            SE_USER => (int) api_get_user_id(),
2776
        ];
2777
        $ic_slide->xapian_data = serialize($xapian_data);
2778
        $exercise_description = $all_specific_terms.' '.$this->description;
2779
        $ic_slide->addValue('content', $exercise_description);
2780
2781
        $di = new ChamiloIndexer();
2782
        isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2783
        $di->connectDb(null, null, $lang);
2784
        $di->addChunk($ic_slide);
2785
2786
        //index and return search engine document id
2787
        $did = $di->index();
2788
        if ($did) {
2789
            // save it to db
2790
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2791
            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2792
			    VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2793
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2794
            Database::query($sql);
2795
        }
2796
    }
2797
2798
    public function search_engine_edit()
2799
    {
2800
        // update search enchine and its values table if enabled
2801
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2802
            $course_id = api_get_course_id();
2803
2804
            // actually, it consists on delete terms from db,
2805
            // insert new ones, create a new search engine document, and remove the old one
2806
            // get search_did
2807
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2808
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
2809
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2810
            $res = Database::query($sql);
2811
2812
            if (Database::num_rows($res) > 0) {
2813
                $se_ref = Database::fetch_array($res);
2814
                $specific_fields = get_specific_field_list();
2815
                $ic_slide = new IndexableChunk();
2816
2817
                $all_specific_terms = '';
2818
                foreach ($specific_fields as $specific_field) {
2819
                    delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->getId());
2820
                    if (isset($_REQUEST[$specific_field['code']])) {
2821
                        $sterms = trim($_REQUEST[$specific_field['code']]);
2822
                        $all_specific_terms .= ' '.$sterms;
2823
                        $sterms = explode(',', $sterms);
2824
                        foreach ($sterms as $sterm) {
2825
                            $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2826
                            add_specific_field_value(
2827
                                $specific_field['id'],
2828
                                $course_id,
2829
                                TOOL_QUIZ,
2830
                                $this->getId(),
2831
                                $sterm
2832
                            );
2833
                        }
2834
                    }
2835
                }
2836
2837
                // build the chunk to index
2838
                $ic_slide->addValue('title', $this->exercise);
2839
                $ic_slide->addCourseId($course_id);
2840
                $ic_slide->addToolId(TOOL_QUIZ);
2841
                $xapian_data = [
2842
                    SE_COURSE_ID => $course_id,
2843
                    SE_TOOL_ID => TOOL_QUIZ,
2844
                    SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2845
                    SE_USER => (int) api_get_user_id(),
2846
                ];
2847
                $ic_slide->xapian_data = serialize($xapian_data);
2848
                $exercise_description = $all_specific_terms.' '.$this->description;
2849
                $ic_slide->addValue('content', $exercise_description);
2850
2851
                $di = new ChamiloIndexer();
2852
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2853
                $di->connectDb(null, null, $lang);
2854
                $di->remove_document($se_ref['search_did']);
2855
                $di->addChunk($ic_slide);
2856
2857
                //index and return search engine document id
2858
                $did = $di->index();
2859
                if ($did) {
2860
                    // save it to db
2861
                    $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
2862
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2863
                    Database::query($sql);
2864
                    $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2865
                        VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2866
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2867
                    Database::query($sql);
2868
                }
2869
            } else {
2870
                $this->search_engine_save();
2871
            }
2872
        }
2873
    }
2874
2875
    public function search_engine_delete()
2876
    {
2877
        // remove from search engine if enabled
2878
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2879
            $course_id = api_get_course_id();
2880
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2881
            $sql = 'SELECT * FROM %s
2882
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2883
                    LIMIT 1';
2884
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2885
            $res = Database::query($sql);
2886
            if (Database::num_rows($res) > 0) {
2887
                $row = Database::fetch_array($res);
2888
                $di = new ChamiloIndexer();
2889
                $di->remove_document($row['search_did']);
2890
                unset($di);
2891
                $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2892
                foreach ($this->questionList as $question_i) {
2893
                    $sql = 'SELECT type FROM %s WHERE id=%s';
2894
                    $sql = sprintf($sql, $tbl_quiz_question, $question_i);
2895
                    $qres = Database::query($sql);
2896
                    if (Database::num_rows($qres) > 0) {
2897
                        $qrow = Database::fetch_array($qres);
2898
                        $objQuestion = Question::getInstance($qrow['type']);
2899
                        $objQuestion = Question::read((int) $question_i);
2900
                        $objQuestion->search_engine_edit($this->getId(), false, true);
2901
                        unset($objQuestion);
2902
                    }
2903
                }
2904
            }
2905
            $sql = 'DELETE FROM %s
2906
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2907
                    LIMIT 1';
2908
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2909
            Database::query($sql);
2910
2911
            // remove terms from db
2912
            delete_all_values_for_item($course_id, TOOL_QUIZ, $this->getId());
2913
        }
2914
    }
2915
2916
    public function selectExpiredTime()
2917
    {
2918
        return $this->expired_time;
2919
    }
2920
2921
    /**
2922
     * Cleans the student's results only for the Exercise tool (Not from the LP)
2923
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
2924
     * Works with exercises in sessions.
2925
     *
2926
     * @param bool   $cleanLpTests
2927
     * @param string $cleanResultBeforeDate
2928
     *
2929
     * @return int quantity of user's exercises deleted
2930
     */
2931
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
2932
    {
2933
        $sessionId = api_get_session_id();
2934
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2935
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
2936
2937
        $sql_where = '  AND
2938
                        orig_lp_id = 0 AND
2939
                        orig_lp_item_id = 0';
2940
2941
        // if we want to delete results from LP too
2942
        if ($cleanLpTests) {
2943
            $sql_where = '';
2944
        }
2945
2946
        // if we want to delete attempts before date $cleanResultBeforeDate
2947
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
2948
        if (!empty($cleanResultBeforeDate)) {
2949
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
2950
            if (api_is_valid_date($cleanResultBeforeDate)) {
2951
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
2952
            } else {
2953
                return 0;
2954
            }
2955
        }
2956
2957
        $sessionCondition = api_get_session_condition($sessionId);
2958
        $sql = "SELECT exe_id
2959
                FROM $table_track_e_exercises
2960
                WHERE
2961
                    c_id = ".api_get_course_int_id().' AND
2962
                    exe_exo_id = '.$this->getId()."
2963
                    $sessionCondition
2964
                    $sql_where";
2965
2966
        $result = Database::query($sql);
2967
        $exe_list = Database::store_result($result);
2968
2969
        // deleting TRACK_E_ATTEMPT table
2970
        // check if exe in learning path or not
2971
        $i = 0;
2972
        if (is_array($exe_list) && count($exe_list) > 0) {
2973
            foreach ($exe_list as $item) {
2974
                $sql = "DELETE FROM $table_track_e_attempt
2975
                        WHERE exe_id = '".$item['exe_id']."'";
2976
                Database::query($sql);
2977
                $i++;
2978
            }
2979
        }
2980
2981
        // delete TRACK_E_EXERCISES table
2982
        $sql = "DELETE FROM $table_track_e_exercises
2983
                WHERE
2984
                  c_id = ".api_get_course_int_id().' AND
2985
                  exe_exo_id = '.$this->getId()." $sql_where $sessionCondition";
2986
        Database::query($sql);
2987
2988
        $this->generateStats($this->getId(), api_get_course_info(), $sessionId);
2989
2990
        Event::addEvent(
2991
            LOG_EXERCISE_RESULT_DELETE,
2992
            LOG_EXERCISE_ID,
2993
            $this->getId(),
2994
            null,
2995
            null,
2996
            api_get_course_int_id(),
2997
            $sessionId
2998
        );
2999
3000
        return $i;
3001
    }
3002
3003
    /**
3004
     * Copies an exercise (duplicate all questions and answers).
3005
     */
3006
    public function copyExercise()
3007
    {
3008
        $exerciseObject = $this;
3009
        $categories = $exerciseObject->getCategoriesInExercise(true);
3010
        // Get all questions no matter the order/category settings
3011
        $questionList = $exerciseObject->getQuestionOrderedList();
3012
        $sourceId = $exerciseObject->iId;
3013
        // Force the creation of a new exercise
3014
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
3015
        $exerciseObject->iId = 0;
3016
        $exerciseObject->sessionId = api_get_session_id();
3017
        $courseId = api_get_course_int_id();
3018
        $exerciseObject->save();
3019
        $newId = $exerciseObject->getId();
3020
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
3021
3022
        $count = 1;
3023
        $batchSize = 20;
3024
        $em = Database::getManager();
3025
        if ($newId && !empty($questionList)) {
3026
            $extraField = new ExtraFieldValue('exercise');
3027
            $extraField->copy($sourceId, $newId);
3028
            // Question creation
3029
            foreach ($questionList as $oldQuestionId) {
3030
                $oldQuestionObj = Question::read($oldQuestionId, null, false);
3031
                $newQuestionId = $oldQuestionObj->duplicate();
3032
                if ($newQuestionId) {
3033
                    $newQuestionObj = Question::read($newQuestionId, null, false);
3034
                    if (isset($newQuestionObj) && $newQuestionObj) {
3035
                        $sql = "INSERT INTO $exerciseRelQuestionTable (question_id, quiz_id, question_order)
3036
                                VALUES (".$newQuestionId.", ".$newId.", '$count')";
3037
                        Database::query($sql);
3038
                        $count++;
3039
                        if (!empty($oldQuestionObj->category)) {
3040
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
3041
                        }
3042
3043
                        // This should be moved to the duplicate function
3044
                        $newAnswerObj = new Answer($oldQuestionId, $courseId, $exerciseObject);
3045
                        $newAnswerObj->read();
3046
                        $newAnswerObj->duplicate($newQuestionObj);
3047
                        if (($count % $batchSize) === 0) {
3048
                            $em->clear(); // Detaches all objects from Doctrine!
3049
                        }
3050
                    }
3051
                }
3052
            }
3053
            if (!empty($categories)) {
3054
                $newCategoryList = [];
3055
                foreach ($categories as $category) {
3056
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
3057
                }
3058
                $exerciseObject->saveCategoriesInExercise($newCategoryList);
3059
            }
3060
        }
3061
    }
3062
3063
    /**
3064
     * Changes the exercise status.
3065
     *
3066
     * @param string $status - exercise status
3067
     */
3068
    public function updateStatus($status)
3069
    {
3070
        $this->active = $status;
3071
    }
3072
3073
    /**
3074
     * @param int    $lp_id
3075
     * @param int    $lp_item_id
3076
     * @param int    $lp_item_view_id
3077
     * @param string $status
3078
     *
3079
     * @return array
3080
     */
3081
    public function get_stat_track_exercise_info(
3082
        $lp_id = 0,
3083
        $lp_item_id = 0,
3084
        $lp_item_view_id = 0,
3085
        $status = 'incomplete'
3086
    ) {
3087
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3088
        $lp_id = (int) $lp_id;
3089
        $lp_item_id = (int) $lp_item_id;
3090
        $lp_item_view_id = (int) $lp_item_view_id;
3091
3092
        $sessionCondition = api_get_session_condition(api_get_session_id());
3093
        $condition = " WHERE exe_exo_id 	= ".$this->getId()." AND
3094
					   exe_user_id 			= '".api_get_user_id()."' AND
3095
					   c_id                 = ".api_get_course_int_id()." AND
3096
					   status 				= '".Database::escape_string($status)."' AND
3097
					   orig_lp_id 			= $lp_id AND
3098
					   orig_lp_item_id 		= $lp_item_id AND
3099
                       orig_lp_item_view_id =  $lp_item_view_id
3100
					   ";
3101
3102
        $sql_track = " SELECT * FROM  $track_exercises $condition $sessionCondition LIMIT 1 ";
3103
3104
        $result = Database::query($sql_track);
3105
        $new_array = [];
3106
        if (Database::num_rows($result) > 0) {
3107
            $new_array = Database::fetch_assoc($result);
3108
            $new_array['num_exe'] = Database::num_rows($result);
3109
        }
3110
3111
        return $new_array;
3112
    }
3113
3114
    /**
3115
     * Saves a test attempt.
3116
     *
3117
     * @param int   $clock_expired_time   clock_expired_time
3118
     * @param int   $safe_lp_id           lp id
3119
     * @param int   $safe_lp_item_id      lp item id
3120
     * @param int   $safe_lp_item_view_id lp item_view id
3121
     * @param array $questionList
3122
     * @param float $weight
3123
     *
3124
     * @throws \Doctrine\ORM\ORMException
3125
     * @throws \Doctrine\ORM\OptimisticLockException
3126
     * @throws \Doctrine\ORM\TransactionRequiredException
3127
     *
3128
     * @return int
3129
     */
3130
    public function save_stat_track_exercise_info(
3131
        $clock_expired_time,
3132
        $safe_lp_id = 0,
3133
        $safe_lp_item_id = 0,
3134
        $safe_lp_item_view_id = 0,
3135
        $questionList = [],
3136
        $weight = 0
3137
    ) {
3138
        $safe_lp_id = (int) $safe_lp_id;
3139
        $safe_lp_item_id = (int) $safe_lp_item_id;
3140
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
3141
3142
        if (empty($clock_expired_time)) {
3143
            $clock_expired_time = null;
3144
        }
3145
3146
        $questionList = array_filter(
3147
            $questionList,
3148
            function (int $qid) {
3149
                $q = Question::read($qid);
3150
                return $q
3151
                    && !in_array(
3152
                        $q->type,
3153
                        [PAGE_BREAK, MEDIA_QUESTION],
3154
                        true
3155
                    );
3156
            }
3157
        );
3158
3159
        $questionList = array_map('intval', $questionList);
3160
        $em = Database::getManager();
3161
3162
        $quiz = $em->find(CQuiz::class, $this->getId());
3163
3164
        $trackExercise = (new TrackEExercise())
3165
            ->setSession(api_get_session_entity())
3166
            ->setCourse(api_get_course_entity())
3167
            ->setMaxScore($weight)
3168
            ->setDataTracking(implode(',', $questionList))
3169
            ->setUser(api_get_user_entity())
3170
            ->setUserIp(api_get_real_ip())
3171
            ->setOrigLpId($safe_lp_id)
3172
            ->setOrigLpItemId($safe_lp_item_id)
3173
            ->setOrigLpItemViewId($safe_lp_item_view_id)
3174
            ->setExpiredTimeControl($clock_expired_time)
3175
            ->setQuiz($quiz)
3176
        ;
3177
        $em->persist($trackExercise);
3178
        $em->flush();
3179
3180
        return $trackExercise->getExeId();
3181
    }
3182
3183
    /**
3184
     * @param int    $question_id
3185
     * @param int    $questionNum
3186
     * @param array  $questions_in_media
3187
     * @param string $currentAnswer
3188
     * @param array  $myRemindList
3189
     * @param bool   $showPreviousButton
3190
     *
3191
     * @return string
3192
     */
3193
    public function show_button(
3194
        $question_id,
3195
        $questionNum,
3196
        $questions_in_media = [],
3197
        $currentAnswer = '',
3198
        $myRemindList = [],
3199
        $showPreviousButton = true
3200
    ) {
3201
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3202
        $nbrQuestions = $this->countQuestionsInExercise();
3203
        $buttonList = [];
3204
        $html = $label = '';
3205
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3206
3207
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3208
            ONE_PER_PAGE == $this->type
3209
        ) {
3210
            $urlTitle = get_lang('Proceed with the test');
3211
            if ($questionNum == count($this->questionList)) {
3212
                $urlTitle = get_lang('End test');
3213
            }
3214
3215
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3216
            $url .= '&'.http_build_query(
3217
                    [
3218
                        'learnpath_id' => $safe_lp_id,
3219
                        'learnpath_item_id' => $safe_lp_item_id,
3220
                        'learnpath_item_view_id' => $safe_lp_item_view_id,
3221
                        'hotspot' => $hotspotGet,
3222
                        'nbrQuestions' => $nbrQuestions,
3223
                        'num' => $questionNum,
3224
                        'exerciseType' => $this->type,
3225
                        'exerciseId' => $this->getId(),
3226
                        'reminder' => empty($myRemindList) ? null : 2,
3227
                        'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3228
                    ]
3229
                );
3230
3231
            $params = [
3232
                'class' => 'ajax btn btn--plain no-close-button',
3233
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3234
                'data-size' => 'md',
3235
                'id' => "button_$question_id",
3236
            ];
3237
3238
            if (EXERCISE_FEEDBACK_TYPE_POPUP === $this->getFeedbackType()) {
3239
                //$params['data-block-div-after-closing'] = "question_div_$question_id";
3240
                $params['data-block-closing'] = 'true';
3241
                $params['class'] .= ' no-header ';
3242
            }
3243
3244
            $html .= Display::url($urlTitle, $url, $params);
3245
            $html .= '<br />';
3246
3247
            return $html;
3248
        }
3249
3250
        if (!api_is_allowed_to_session_edit()) {
3251
            return '';
3252
        }
3253
3254
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 == $_REQUEST['reminder'];
3255
3256
        // User
3257
        $endReminderValue = false;
3258
        if (!empty($myRemindList) && $isReviewingAnswers) {
3259
            $endValue = end($myRemindList);
3260
            if ($endValue == $question_id) {
3261
                $endReminderValue = true;
3262
            }
3263
        }
3264
        $endTest = false;
3265
        if (ALL_ON_ONE_PAGE == $this->type || $nbrQuestions == $questionNum || $endReminderValue) {
3266
            if ($this->review_answers) {
3267
                $label = get_lang('Review selected questions');
3268
                $class = 'btn btn--success';
3269
            } else {
3270
                $endTest = true;
3271
                $label = get_lang('End test');
3272
                $class = 'btn btn--warning';
3273
            }
3274
        } else {
3275
            $label = get_lang('Next question');
3276
            $class = 'btn btn--primary';
3277
        }
3278
        // used to select it with jquery
3279
        $class .= ' question-validate-btn';
3280
        if (ONE_PER_PAGE == $this->type) {
3281
            if (1 != $questionNum && $this->showPreviousButton()) {
3282
                $prev_question = $questionNum - 2;
3283
                $showPreview = true;
3284
                if (!empty($myRemindList) && $isReviewingAnswers) {
3285
                    $beforeId = null;
3286
                    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...
3287
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3288
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3289
3290
                            break;
3291
                        }
3292
                    }
3293
3294
                    if (empty($beforeId)) {
3295
                        $showPreview = false;
3296
                    } else {
3297
                        $num = 0;
3298
                        foreach ($this->questionList as $originalQuestionId) {
3299
                            if ($originalQuestionId == $beforeId) {
3300
                                break;
3301
                            }
3302
                            $num++;
3303
                        }
3304
                        $prev_question = $num;
3305
                    }
3306
                }
3307
3308
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3309
                    $buttonList[] = Display::button(
3310
                        'previous_question_and_save',
3311
                        get_lang('Previous question'),
3312
                        [
3313
                            'type' => 'button',
3314
                            'class' => 'btn btn--plain',
3315
                            'data-prev' => $prev_question,
3316
                            'data-question' => $question_id,
3317
                        ]
3318
                    );
3319
                }
3320
            }
3321
3322
            // Next question
3323
            if (!empty($questions_in_media)) {
3324
                $buttonList[] = Display::button(
3325
                    'save_question_list',
3326
                    $label,
3327
                    [
3328
                        'type' => 'button',
3329
                        'class' => $class,
3330
                        'data-list' => implode(',', $questions_in_media),
3331
                    ]
3332
                );
3333
            } else {
3334
                $attributes = ['type' => 'button', 'class' => $class, 'data-question' => $question_id];
3335
                $name = 'save_now';
3336
                if ($endTest && api_get_configuration_value('quiz_check_all_answers_before_end_test')) {
3337
                    $name = 'check_answers';
3338
                }
3339
                $buttonList[] = Display::button(
3340
                    $name,
3341
                    $label,
3342
                    $attributes
3343
                );
3344
            }
3345
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3346
3347
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3348
3349
            return $html;
3350
        }
3351
3352
        if ($this->review_answers) {
3353
            $all_label = get_lang('Review selected questions');
3354
            $class = 'btn btn--success';
3355
        } else {
3356
            $all_label = get_lang('End test');
3357
            $class = 'btn btn--warning';
3358
        }
3359
        // used to select it with jquery
3360
        $class .= ' question-validate-btn';
3361
        $buttonList[] = Display::button(
3362
            'validate_all',
3363
            $all_label,
3364
            ['type' => 'button', 'class' => $class]
3365
        );
3366
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3367
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3368
3369
        return $html;
3370
    }
3371
3372
    /**
3373
     * @param int    $timeLeft in seconds
3374
     * @param string $url
3375
     *
3376
     * @return string
3377
     */
3378
    public function showSimpleTimeControl($timeLeft, $url = '')
3379
    {
3380
        $timeLeft = (int) $timeLeft;
3381
3382
        return "<script>
3383
            function openClockWarning() {
3384
                $('#clock_warning').dialog({
3385
                    modal:true,
3386
                    height:320,
3387
                    width:550,
3388
                    closeOnEscape: false,
3389
                    resizable: false,
3390
                    buttons: {
3391
                        '".addslashes(get_lang('Close'))."': function() {
3392
                            $('#clock_warning').dialog('close');
3393
                        }
3394
                    },
3395
                    close: function() {
3396
                        window.location.href = '$url';
3397
                    }
3398
                });
3399
                $('#clock_warning').dialog('open');
3400
                $('#counter_to_redirect').epiclock({
3401
                    mode: $.epiclock.modes.countdown,
3402
                    offset: {seconds: 5},
3403
                    format: 's'
3404
                }).bind('timer', function () {
3405
                    window.location.href = '$url';
3406
                });
3407
            }
3408
3409
            function onExpiredTimeExercise() {
3410
                $('#wrapper-clock').hide();
3411
                $('#expired-message-id').show();
3412
                // Fixes bug #5263
3413
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3414
                openClockWarning();
3415
            }
3416
3417
			$(function() {
3418
				// time in seconds when using minutes there are some seconds lost
3419
                var time_left = parseInt(".$timeLeft.");
3420
                $('#exercise_clock_warning').epiclock({
3421
                    mode: $.epiclock.modes.countdown,
3422
                    offset: {seconds: time_left},
3423
                    format: 'x:i:s',
3424
                    renderer: 'minute'
3425
                }).bind('timer', function () {
3426
                    onExpiredTimeExercise();
3427
                });
3428
	       		$('#submit_save').click(function () {});
3429
	        });
3430
	    </script>";
3431
    }
3432
3433
    /**
3434
     * So the time control will work.
3435
     *
3436
     * @param int    $timeLeft
3437
     * @param string $redirectToUrl
3438
     *
3439
     * @return string
3440
     */
3441
    public function showTimeControlJS($timeLeft, $redirectToUrl = '')
3442
    {
3443
        $timeLeft = (int) $timeLeft;
3444
        $script = 'redirectExerciseToResult();';
3445
        if (ALL_ON_ONE_PAGE == $this->type) {
3446
            $script = "save_now_all('validate');";
3447
        } elseif (ONE_PER_PAGE == $this->type) {
3448
            $script = 'window.quizTimeEnding = true;
3449
                $(\'[name="save_now"]\').trigger(\'click\');';
3450
        }
3451
3452
        $exerciseSubmitRedirect = '';
3453
        if (!empty($redirectToUrl)) {
3454
            $exerciseSubmitRedirect = "window.location = '$redirectToUrl'";
3455
        }
3456
3457
        return "<script>
3458
            function openClockWarning() {
3459
                $('#clock_warning').dialog({
3460
                    modal:true,
3461
                    height:320,
3462
                    width:550,
3463
                    closeOnEscape: false,
3464
                    resizable: false,
3465
                    buttons: {
3466
                        '".addslashes(get_lang('End test'))."': function() {
3467
                            $('#clock_warning').dialog('close');
3468
                        }
3469
                    },
3470
                    close: function() {
3471
                        send_form();
3472
                    }
3473
                });
3474
3475
                $('#clock_warning').dialog('open');
3476
                $('#counter_to_redirect').epiclock({
3477
                    mode: $.epiclock.modes.countdown,
3478
                    offset: {seconds: 5},
3479
                    format: 's'
3480
                }).bind('timer', function () {
3481
                    send_form();
3482
                });
3483
            }
3484
3485
            function send_form() {
3486
                if ($('#exercise_form').length) {
3487
                    $script
3488
                } else {
3489
                    $exerciseSubmitRedirect
3490
                    // In exercise_reminder.php
3491
                    final_submit();
3492
                }
3493
            }
3494
3495
            function onExpiredTimeExercise() {
3496
                $('#wrapper-clock').hide();
3497
                $('#expired-message-id').show();
3498
                // Fixes bug #5263
3499
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3500
                openClockWarning();
3501
            }
3502
3503
			$(function() {
3504
				// time in seconds when using minutes there are some seconds lost
3505
                var time_left = parseInt(".$timeLeft.");
3506
                $('#exercise_clock_warning').epiclock({
3507
                    mode: $.epiclock.modes.countdown,
3508
                    offset: {seconds: time_left},
3509
                    format: 'x:C:s',
3510
                    renderer: 'minute'
3511
                }).bind('timer', function () {
3512
                    onExpiredTimeExercise();
3513
                });
3514
	       		$('#submit_save').click(function () {});
3515
	        });
3516
	    </script>";
3517
    }
3518
3519
    /**
3520
     * This function was originally found in the exercise_show.php.
3521
     *
3522
     * @param int    $exeId
3523
     * @param int    $questionId
3524
     * @param mixed  $choice                                    the user-selected option
3525
     * @param string $from                                      function is called from 'exercise_show' or
3526
     *                                                          'exercise_result'
3527
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3528
     *                                                          coordinates
3529
     * @param bool   $save_results                              save results in the DB or just show the response
3530
     * @param bool   $from_database                             gets information from DB or from the current selection
3531
     * @param bool   $show_result                               show results or not
3532
     * @param int    $propagate_neg
3533
     * @param array  $hotspot_delineation_result
3534
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3535
     * @param bool   $updateResults
3536
     * @param bool   $showHotSpotDelineationTable
3537
     * @param int    $questionDuration                          seconds
3538
     *
3539
     * @return string html code
3540
     *
3541
     * @todo    reduce parameters of this function
3542
     */
3543
    public function manage_answer(
3544
        $exeId,
3545
        $questionId,
3546
        $choice,
3547
        $from = 'exercise_show',
3548
        $exerciseResultCoordinates = [],
3549
        $save_results = true,
3550
        $from_database = false,
3551
        $show_result = true,
3552
        $propagate_neg = 0,
3553
        $hotspot_delineation_result = [],
3554
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3555
        $updateResults = false,
3556
        $showHotSpotDelineationTable = false,
3557
        $questionDuration = 0
3558
    ) {
3559
        $debug = false;
3560
        //needed in order to use in the exercise_attempt() for the time
3561
        global $learnpath_id, $learnpath_item_id;
3562
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3563
        $em = Database::getManager();
3564
        $feedback_type = $this->getFeedbackType();
3565
        $results_disabled = $this->selectResultsDisabled();
3566
        $questionDuration = (int) $questionDuration;
3567
        $hotspotsSavedEarly = false;
3568
3569
        if ($debug) {
3570
            error_log('<------ manage_answer ------> ');
3571
            error_log('exe_id: '.$exeId);
3572
            error_log('$from:  '.$from);
3573
            error_log('$save_results: '.(int) $save_results);
3574
            error_log('$from_database: '.(int) $from_database);
3575
            error_log('$show_result: '.(int) $show_result);
3576
            error_log('$propagate_neg: '.$propagate_neg);
3577
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3578
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3579
            error_log('$learnpath_id: '.$learnpath_id);
3580
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3581
            error_log('$choice: '.print_r($choice, 1));
3582
            error_log('-----------------------------');
3583
        }
3584
3585
        $final_overlap = 0;
3586
        $final_missing = 0;
3587
        $final_excess = 0;
3588
        $overlap_color = 0;
3589
        $missing_color = 0;
3590
        $excess_color = 0;
3591
        $threadhold1 = 0;
3592
        $threadhold2 = 0;
3593
        $threadhold3 = 0;
3594
        $arrques = null;
3595
        $arrans = null;
3596
        $studentChoice = null;
3597
        $expectedAnswer = '';
3598
        $calculatedChoice = '';
3599
        $calculatedStatus = '';
3600
        $questionId = (int) $questionId;
3601
        $exeId = (int) $exeId;
3602
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3603
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3604
        $studentChoiceDegree = null;
3605
        $listCorrectAnswers = [];
3606
3607
        // Creates a temporary Question object
3608
        $course_id = $this->course_id;
3609
        $objQuestionTmp = Question::read($questionId, $this->course);
3610
3611
        if (false === $objQuestionTmp) {
3612
            return false;
3613
        }
3614
3615
        $questionName = $objQuestionTmp->selectTitle();
3616
        $questionWeighting = $objQuestionTmp->selectWeighting();
3617
        $answerType = $objQuestionTmp->selectType();
3618
        $quesId = $objQuestionTmp->getId();
3619
        $extra = $objQuestionTmp->extra;
3620
        $next = 1; //not for now
3621
        $totalWeighting = 0;
3622
        $totalScore = 0;
3623
3624
        // Extra information of the question
3625
        if ((
3626
                MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
3627
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
3628
            )
3629
            && !empty($extra)
3630
        ) {
3631
            $extra = explode(':', $extra);
3632
            // Fixes problems with negatives values using intval
3633
            $true_score = (float) trim($extra[0]);
3634
            $false_score = (float) trim($extra[1]);
3635
            $doubt_score = (float) trim($extra[2]);
3636
        }
3637
3638
        // Construction of the Answer object
3639
        $objAnswerTmp = new Answer($questionId, $course_id);
3640
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3641
3642
        if ($debug) {
3643
            error_log('Count of possible answers: '.$nbrAnswers);
3644
            error_log('$answerType: '.$answerType);
3645
        }
3646
3647
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
3648
            $choiceTmp = $choice;
3649
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3650
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3651
        }
3652
3653
        if (FREE_ANSWER == $answerType ||
3654
            ORAL_EXPRESSION == $answerType ||
3655
            CALCULATED_ANSWER == $answerType ||
3656
            ANNOTATION == $answerType
3657
        ) {
3658
            $nbrAnswers = 1;
3659
        }
3660
3661
        $generatedFilesHtml = '';
3662
        if ($answerType == ORAL_EXPRESSION) {
3663
            $generatedFilesHtml = ExerciseLib::getOralFileAudio($exeId, $questionId);
3664
        }
3665
3666
        $user_answer = '';
3667
        // Get answer list for matching
3668
        $sql = "SELECT iid, answer
3669
                FROM $table_ans
3670
                WHERE question_id = $questionId";
3671
        $res_answer = Database::query($sql);
3672
3673
        $answerMatching = [];
3674
        while ($real_answer = Database::fetch_array($res_answer)) {
3675
            $answerMatching[$real_answer['iid']] = $real_answer['answer'];
3676
        }
3677
3678
        // Get first answer needed for global question, no matter the answer shuffle option;
3679
        $firstAnswer = [];
3680
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
3681
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
3682
        ) {
3683
            $sql = "SELECT *
3684
                    FROM $table_ans
3685
                    WHERE question_id = $questionId
3686
                    ORDER BY position
3687
                    LIMIT 1";
3688
            $result = Database::query($sql);
3689
            if (Database::num_rows($result)) {
3690
                $firstAnswer = Database::fetch_array($result);
3691
            }
3692
        }
3693
3694
        $real_answers = [];
3695
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
3696
3697
        $organs_at_risk_hit = 0;
3698
        $questionScore = 0;
3699
        $orderedHotSpots = [];
3700
        if (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, ANNOTATION], true)) {
3701
            $orderedHotSpots = $em->getRepository(TrackEHotspot::class)->findBy(
3702
                [
3703
                    'hotspotQuestionId' => $questionId,
3704
                    'course' => $course_id,
3705
                    'hotspotExeId' => $exeId,
3706
                ],
3707
                ['hotspotAnswerId' => 'ASC']
3708
            );
3709
        }
3710
3711
        if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION], true)) {
3712
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION == $answerType) {
3713
                $questionScore = $questionWeighting;
3714
            }
3715
            // Load student's selected choices
3716
            if ($from_database) {
3717
                $studentChoices = Database::store_result(
3718
                    Database::query(
3719
                        "SELECT answer FROM $TBL_TRACK_ATTEMPT WHERE exe_id = $exeId AND question_id = $questionId"
3720
                    ),
3721
                    'ASSOC'
3722
                );
3723
                $studentChoices = array_column($studentChoices, 'answer');
3724
            } else {
3725
                $studentChoices = array_values((array) $choice);
3726
            }
3727
3728
            // Compute the list of correct choice IDs
3729
            $correctChoices = array_keys(array_filter(
3730
                $answerMatching, // iid => answer text
3731
                function ($answerIdKey) use ($objAnswerTmp) {
3732
                    // Safeguards in case arrays are not initialized
3733
                    $iidList = is_array($objAnswerTmp->iid) ? $objAnswerTmp->iid : [];
3734
                    $pos = array_search($answerIdKey, $iidList, true);
3735
                    return $pos !== false && !empty($objAnswerTmp->correct[$pos]);
3736
                },
3737
                ARRAY_FILTER_USE_KEY
3738
            ));
3739
3740
            // Scoring
3741
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION === $answerType) {
3742
                // All-or-nothing scoring
3743
                $questionScore = (float) $questionWeighting;
3744
                if (array_diff($studentChoices, $correctChoices) || array_diff($correctChoices, $studentChoices)) {
3745
                    $questionScore = 0.0;
3746
                }
3747
            } else { // MULTIPLE_ANSWER_DROPDOWN
3748
                // Partial scoring using per-choice weighting
3749
                $questionScore = 0.0;
3750
                $iidList    = is_array($objAnswerTmp->iid) ? $objAnswerTmp->iid : [];
3751
                $correctArr = is_array($objAnswerTmp->correct) ? $objAnswerTmp->correct : [];
3752
                $weights    = is_array($objAnswerTmp->weighting) ? $objAnswerTmp->weighting : [];
3753
3754
                foreach ((array) $studentChoices as $choiceId) {
3755
                    $pos = array_search($choiceId, $iidList, true);
3756
                    if ($pos !== false && !empty($correctArr[$pos])) {
3757
                        $questionScore += (float) ($weights[$pos] ?? 0);
3758
                    }
3759
                }
3760
            }
3761
3762
            // Render (UI)
3763
            if ($show_result) {
3764
                echo ExerciseShowFunctions::displayMultipleAnswerDropdown(
3765
                    $this,
3766
                    $objAnswerTmp,
3767
                    $correctChoices,
3768
                    $studentChoices,
3769
                    $showTotalScoreAndUserChoicesInLastAttempt
3770
                );
3771
            }
3772
        }
3773
3774
        if ($debug) {
3775
            error_log('-- Start answer loop --');
3776
        }
3777
3778
        $answerDestination = null;
3779
        $userAnsweredQuestion = false;
3780
        $correctAnswerId = [];
3781
        $matchingCorrectAnswers = [];
3782
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3783
            $answer = $objAnswerTmp->selectAnswer($answerId);
3784
            $answerComment = $objAnswerTmp->selectComment($answerId);
3785
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3786
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3787
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3788
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3789
3790
            if ($debug) {
3791
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3792
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3793
                error_log("answerWeighting: $answerWeighting");
3794
            }
3795
3796
            // Delineation
3797
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3798
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3799
3800
            switch ($answerType) {
3801
                case UNIQUE_ANSWER:
3802
                case UNIQUE_ANSWER_IMAGE:
3803
                case UNIQUE_ANSWER_NO_OPTION:
3804
                case READING_COMPREHENSION:
3805
                    if ($from_database) {
3806
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3807
                                WHERE
3808
                                    exe_id = $exeId AND
3809
                                    question_id = $questionId";
3810
                        $result = Database::query($sql);
3811
                        $choice = Database::result($result, 0, 'answer');
3812
3813
                        if (false === $userAnsweredQuestion) {
3814
                            $userAnsweredQuestion = !empty($choice);
3815
                        }
3816
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3817
                        if ($studentChoice) {
3818
                            $questionScore += $answerWeighting;
3819
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3820
                            $correctAnswerId[] = $answerId;
3821
                        }
3822
                    } else {
3823
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3824
                        if ($studentChoice) {
3825
                            $questionScore += $answerWeighting;
3826
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3827
                            $correctAnswerId[] = $answerId;
3828
                        }
3829
                    }
3830
3831
                    break;
3832
                case MULTIPLE_ANSWER_TRUE_FALSE:
3833
                    if ($from_database) {
3834
                        $choice = [];
3835
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3836
                                WHERE
3837
                                    exe_id = $exeId AND
3838
                                    question_id = ".$questionId;
3839
3840
                        $result = Database::query($sql);
3841
                        while ($row = Database::fetch_array($result)) {
3842
                            $values = explode(':', $row['answer']);
3843
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3844
                            $option = isset($values[1]) ? $values[1] : '';
3845
                            $choice[$my_answer_id] = $option;
3846
                        }
3847
                        $userAnsweredQuestion = !empty($choice);
3848
                    }
3849
3850
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3851
                    if (!empty($studentChoice)) {
3852
                        $correctAnswerId[] = $answerAutoId;
3853
                        if ($studentChoice == $answerCorrect) {
3854
                            $questionScore += $true_score;
3855
                        } else {
3856
                            if (isset($quiz_question_options[$studentChoice])
3857
                                && in_array($quiz_question_options[$studentChoice]['name'], ["Don't know", 'DoubtScore'])
3858
                            ) {
3859
                                $questionScore += $doubt_score;
3860
                            } else {
3861
                                $questionScore += $false_score;
3862
                            }
3863
                        }
3864
                    } else {
3865
                        // If no result then the user just hit don't know
3866
                        $studentChoice = 3;
3867
                        $questionScore += $doubt_score;
3868
                    }
3869
                    $totalScore = $questionScore;
3870
3871
                    break;
3872
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3873
                    if ($from_database) {
3874
                        $choice = [];
3875
                        $choiceDegreeCertainty = [];
3876
                        $sql = "SELECT answer
3877
                                FROM $TBL_TRACK_ATTEMPT
3878
                                WHERE exe_id = $exeId AND question_id = $questionId";
3879
3880
                        $result = Database::query($sql);
3881
                        while ($row = Database::fetch_array($result)) {
3882
                            $ind = $row['answer'];
3883
                            $values = explode(':', $ind);
3884
                            $myAnswerId = $values[0] ?? null;
3885
                            $option = $values[1] ?? null;
3886
                            $percent = $values[2] ?? null;
3887
                            $choice[$myAnswerId] = $option;
3888
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3889
                        }
3890
                    }
3891
3892
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3893
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3894
3895
                    // student score update
3896
                    if (!empty($studentChoice)) {
3897
                        if ($studentChoice == $answerCorrect) {
3898
                            // correct answer and student is Unsure or PrettySur
3899
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3900
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3901
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3902
                            ) {
3903
                                $questionScore += $true_score;
3904
                            } else {
3905
                                // student ignore correct answer
3906
                                $questionScore += $doubt_score;
3907
                            }
3908
                        } else {
3909
                            // false answer and student is Unsure or PrettySur
3910
                            if (isset($quiz_question_options[$studentChoiceDegree]) && $quiz_question_options[$studentChoiceDegree]['position'] >= 3
3911
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3912
                                $questionScore += $false_score;
3913
                            } else {
3914
                                // student ignore correct answer
3915
                                $questionScore += $doubt_score;
3916
                            }
3917
                        }
3918
                    }
3919
                    $totalScore = $questionScore;
3920
3921
                    break;
3922
                case MULTIPLE_ANSWER:
3923
                case MULTIPLE_ANSWER_DROPDOWN:
3924
                    if ($from_database) {
3925
                        $choice = [];
3926
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3927
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3928
                        $resultans = Database::query($sql);
3929
                        while ($row = Database::fetch_array($resultans)) {
3930
                            $choice[$row['answer']] = 1;
3931
                        }
3932
3933
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3934
                        $real_answers[$answerId] = (bool) $studentChoice;
3935
3936
                        if ($studentChoice) {
3937
                            $questionScore += $answerWeighting;
3938
                        }
3939
                    } else {
3940
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3941
                        $real_answers[$answerId] = (bool) $studentChoice;
3942
3943
                        if (isset($studentChoice)
3944
                            || (MULTIPLE_ANSWER_DROPDOWN == $answerType && in_array($answerAutoId, $choice))
3945
                        ) {
3946
                            $correctAnswerId[] = $answerAutoId;
3947
                            $questionScore += $answerWeighting;
3948
                        }
3949
                    }
3950
                    $totalScore += $answerWeighting;
3951
3952
                    break;
3953
                case GLOBAL_MULTIPLE_ANSWER:
3954
                    if ($from_database) {
3955
                        $choice = [];
3956
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3957
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3958
                        $resultans = Database::query($sql);
3959
                        while ($row = Database::fetch_array($resultans)) {
3960
                            $choice[$row['answer']] = 1;
3961
                        }
3962
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3963
                        $real_answers[$answerId] = (bool) $studentChoice;
3964
                        if ($studentChoice) {
3965
                            $questionScore += $answerWeighting;
3966
                        }
3967
                    } else {
3968
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3969
                        if (isset($studentChoice)) {
3970
                            $questionScore += $answerWeighting;
3971
                        }
3972
                        $real_answers[$answerId] = (bool) $studentChoice;
3973
                    }
3974
                    $totalScore += $answerWeighting;
3975
                    if ($debug) {
3976
                        error_log("studentChoice: $studentChoice");
3977
                    }
3978
3979
                    break;
3980
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3981
                    if ($from_database) {
3982
                        $choice = [];
3983
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3984
                                WHERE exe_id = $exeId AND question_id = $questionId";
3985
                        $resultans = Database::query($sql);
3986
                        while ($row = Database::fetch_array($resultans)) {
3987
                            $result = explode(':', $row['answer']);
3988
                            if (isset($result[0])) {
3989
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3990
                                $option = isset($result[1]) ? $result[1] : '';
3991
                                $choice[$my_answer_id] = $option;
3992
                            }
3993
                        }
3994
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3995
3996
                        $real_answers[$answerId] = false;
3997
                        if ($answerCorrect == $studentChoice) {
3998
                            $real_answers[$answerId] = true;
3999
                        }
4000
                    } else {
4001
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
4002
                        $real_answers[$answerId] = false;
4003
                        if ($answerCorrect == $studentChoice) {
4004
                            $real_answers[$answerId] = true;
4005
                        }
4006
                    }
4007
4008
                    break;
4009
                case MULTIPLE_ANSWER_COMBINATION:
4010
                    if ($from_database) {
4011
                        $choice = [];
4012
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4013
                                WHERE exe_id = $exeId AND question_id = $questionId";
4014
                        $resultans = Database::query($sql);
4015
                        while ($row = Database::fetch_array($resultans)) {
4016
                            $choice[$row['answer']] = 1;
4017
                        }
4018
4019
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4020
                        if (1 == $answerCorrect) {
4021
                            $real_answers[$answerId] = false;
4022
                            if ($studentChoice) {
4023
                                $real_answers[$answerId] = true;
4024
                            }
4025
                        } else {
4026
                            $real_answers[$answerId] = true;
4027
                            if ($studentChoice) {
4028
                                $real_answers[$answerId] = false;
4029
                            }
4030
                        }
4031
                    } else {
4032
                        $studentChoice = $choice[$answerAutoId] ?? null;
4033
                        if (1 == $answerCorrect) {
4034
                            $real_answers[$answerId] = false;
4035
                            if ($studentChoice) {
4036
                                $real_answers[$answerId] = true;
4037
                            }
4038
                        } else {
4039
                            $real_answers[$answerId] = true;
4040
                            if ($studentChoice) {
4041
                                $real_answers[$answerId] = false;
4042
                            }
4043
                        }
4044
                    }
4045
4046
                    break;
4047
                case FILL_IN_BLANKS:
4048
                case FILL_IN_BLANKS_COMBINATION:
4049
                    $str = '';
4050
                    $answerFromDatabase = '';
4051
                    if ($from_database) {
4052
                        $sql = "SELECT answer
4053
                                FROM $TBL_TRACK_ATTEMPT
4054
                                WHERE
4055
                                    exe_id = $exeId AND
4056
                                    question_id= $questionId ";
4057
                        $result = Database::query($sql);
4058
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
4059
                    }
4060
4061
                    // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
4062
                    if (false) {
4063
                        // the question is encoded like this
4064
                        // [A] B [C] D [E] F::10,10,10@1
4065
                        // number 1 before the "@" means that is a switchable fill in blank question
4066
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4067
                        // means that is a normal fill blank question
4068
                        // first we explode the "::"
4069
                        $pre_array = explode('::', $answer);
4070
4071
                        // is switchable fill blank or not
4072
                        $last = count($pre_array) - 1;
4073
                        $is_set_switchable = explode('@', $pre_array[$last]);
4074
                        $switchable_answer_set = false;
4075
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4076
                            $switchable_answer_set = true;
4077
                        }
4078
                        $answer = '';
4079
                        for ($k = 0; $k < $last; $k++) {
4080
                            $answer .= $pre_array[$k];
4081
                        }
4082
                        // splits weightings that are joined with a comma
4083
                        $answerWeighting = explode(',', $is_set_switchable[0]);
4084
                        // we save the answer because it will be modified
4085
                        $temp = $answer;
4086
                        $answer = '';
4087
                        $j = 0;
4088
                        //initialise answer tags
4089
                        $user_tags = $correct_tags = $real_text = [];
4090
                        // the loop will stop at the end of the text
4091
                        while (1) {
4092
                            // quits the loop if there are no more blanks (detect '[')
4093
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4094
                                // adds the end of the text
4095
                                $answer = $temp;
4096
                                $real_text[] = $answer;
4097
4098
                                break; //no more "blanks", quit the loop
4099
                            }
4100
                            // adds the piece of text that is before the blank
4101
                            //and ends with '[' into a general storage array
4102
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4103
                            $answer .= api_substr($temp, 0, $pos + 1);
4104
                            //take the string remaining (after the last "[" we found)
4105
                            $temp = api_substr($temp, $pos + 1);
4106
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4107
                            if (false === ($pos = api_strpos($temp, ']'))) {
4108
                                // adds the end of the text
4109
                                $answer .= $temp;
4110
4111
                                break;
4112
                            }
4113
                            if ($from_database) {
4114
                                $str = $answerFromDatabase;
4115
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4116
                                $str = str_replace('\r\n', '', $str);
4117
4118
                                $choice = $arr[1];
4119
                                if (isset($choice[$j])) {
4120
                                    $tmp = api_strrpos($choice[$j], ' / ');
4121
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4122
                                    $choice[$j] = trim($choice[$j]);
4123
                                    // Needed to let characters ' and " to work as part of an answer
4124
                                    $choice[$j] = stripslashes($choice[$j]);
4125
                                } else {
4126
                                    $choice[$j] = null;
4127
                                }
4128
                            } else {
4129
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4130
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4131
                            }
4132
4133
                            $user_tags[] = $choice[$j];
4134
                            // Put the contents of the [] answer tag into correct_tags[]
4135
                            $correct_tags[] = api_substr($temp, 0, $pos);
4136
                            $j++;
4137
                            $temp = api_substr($temp, $pos + 1);
4138
                        }
4139
                        $answer = '';
4140
                        $real_correct_tags = $correct_tags;
4141
                        $chosen_list = [];
4142
4143
                        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...
4144
                            if (0 == $i) {
4145
                                $answer .= $real_text[0];
4146
                            }
4147
                            if (!$switchable_answer_set) {
4148
                                // Needed to parse ' and " characters
4149
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4150
                                if ($correct_tags[$i] == $user_tags[$i]) {
4151
                                    // gives the related weighting to the student
4152
                                    $questionScore += $answerWeighting[$i];
4153
                                    // increments total score
4154
                                    $totalScore += $answerWeighting[$i];
4155
                                    // adds the word in green at the end of the string
4156
                                    $answer .= $correct_tags[$i];
4157
                                } elseif (!empty($user_tags[$i])) {
4158
                                    // else if the word entered by the student IS NOT the same as
4159
                                    // the one defined by the professor
4160
                                    // adds the word in red at the end of the string, and strikes it
4161
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4162
                                } else {
4163
                                    // adds a tabulation if no word has been typed by the student
4164
                                    $answer .= ''; // remove &nbsp; that causes issue
4165
                                }
4166
                            } else {
4167
                                // switchable fill in the blanks
4168
                                if (in_array($user_tags[$i], $correct_tags)) {
4169
                                    $chosen_list[] = $user_tags[$i];
4170
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4171
                                    // gives the related weighting to the student
4172
                                    $questionScore += $answerWeighting[$i];
4173
                                    // increments total score
4174
                                    $totalScore += $answerWeighting[$i];
4175
                                    // adds the word in green at the end of the string
4176
                                    $answer .= $user_tags[$i];
4177
                                } elseif (!empty($user_tags[$i])) {
4178
                                    // else if the word entered by the student IS NOT the same
4179
                                    // as the one defined by the professor
4180
                                    // adds the word in red at the end of the string, and strikes it
4181
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4182
                                } else {
4183
                                    // adds a tabulation if no word has been typed by the student
4184
                                    $answer .= ''; // remove &nbsp; that causes issue
4185
                                }
4186
                            }
4187
4188
                            // adds the correct word, followed by ] to close the blank
4189
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4190
                            if (isset($real_text[$i + 1])) {
4191
                                $answer .= $real_text[$i + 1];
4192
                            }
4193
                        }
4194
                    } else {
4195
                        // insert the student result in the track_e_attempt table, field answer
4196
                        // $answer is the answer like in the c_quiz_answer table for the question
4197
                        // student data are choice[]
4198
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4199
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4200
                        $answerWeighting = $listCorrectAnswers['weighting'];
4201
                        // user choices is an array $choice
4202
4203
                        // get existing user data in n the BDD
4204
                        if ($from_database) {
4205
                            $listStudentResults = FillBlanks::getAnswerInfo(
4206
                                $answerFromDatabase,
4207
                                true
4208
                            );
4209
                            $choice = $listStudentResults['student_answer'];
4210
                        }
4211
4212
                        // loop other all blanks words
4213
                        if (!$switchableAnswerSet) {
4214
                            // not switchable answer, must be in the same place than teacher order
4215
                            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...
4216
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4217
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4218
4219
                                if ($debug) {
4220
                                    error_log("Student answer: $i");
4221
                                    error_log($studentAnswer);
4222
                                }
4223
4224
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4225
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4226
                                // ENT_QUOTES is used in order to transform ' to &#039;
4227
                                if (!$from_database) {
4228
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4229
                                    if ($debug) {
4230
                                        error_log('Student answer cleaned:');
4231
                                        error_log($studentAnswer);
4232
                                    }
4233
                                }
4234
4235
                                $isAnswerCorrect = 0;
4236
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4237
                                    // gives the related weighting to the student
4238
                                    $questionScore += $answerWeighting[$i];
4239
                                    // increments total score
4240
                                    $totalScore += $answerWeighting[$i];
4241
                                    $isAnswerCorrect = 1;
4242
                                }
4243
                                if ($debug) {
4244
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4245
                                }
4246
4247
                                $studentAnswerToShow = $studentAnswer;
4248
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4249
                                if ($debug) {
4250
                                    error_log("Fill in blank type: $type");
4251
                                }
4252
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4253
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4254
                                    if ('' != $studentAnswer) {
4255
                                        foreach ($listMenu as $item) {
4256
                                            if (sha1($item) == $studentAnswer) {
4257
                                                $studentAnswerToShow = $item;
4258
                                            }
4259
                                        }
4260
                                    }
4261
                                }
4262
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4263
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4264
                            }
4265
                        } else {
4266
                            // switchable answer
4267
                            $listStudentAnswerTemp = $choice;
4268
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4269
4270
                            // for every teacher answer, check if there is a student answer
4271
                            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...
4272
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4273
                                $studentAnswerToShow = $studentAnswer;
4274
4275
                                if (empty($studentAnswer)) {
4276
                                    break;
4277
                                }
4278
4279
                                if ($debug) {
4280
                                    error_log("Student answer: $i");
4281
                                    error_log($studentAnswer);
4282
                                }
4283
4284
                                if (!$from_database) {
4285
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4286
                                    if ($debug) {
4287
                                        error_log("Student answer cleaned:");
4288
                                        error_log($studentAnswer);
4289
                                    }
4290
                                }
4291
4292
                                $found = false;
4293
                                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...
4294
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4295
4296
                                    if (!$found) {
4297
                                        if (FillBlanks::isStudentAnswerGood(
4298
                                            $studentAnswer,
4299
                                            $correctAnswer,
4300
                                            $from_database
4301
                                        )) {
4302
                                            $questionScore += $answerWeighting[$i];
4303
                                            $totalScore += $answerWeighting[$i];
4304
                                            $listTeacherAnswerTemp[$j] = '';
4305
                                            $found = true;
4306
                                        }
4307
                                    }
4308
4309
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4310
                                    if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4311
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4312
                                        if (!empty($studentAnswer)) {
4313
                                            foreach ($listMenu as $key => $item) {
4314
                                                if (sha1($item) === $studentAnswer) {
4315
                                                    $studentAnswerToShow = $item;
4316
                                                    break;
4317
                                                }
4318
                                            }
4319
                                        }
4320
                                    }
4321
                                }
4322
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4323
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4324
                            }
4325
                        }
4326
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4327
                    }
4328
4329
                    break;
4330
                case CALCULATED_ANSWER:
4331
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4332
                    if (!empty($calculatedAnswerList)) {
4333
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4334
                        $preArray = explode('@@', $answer);
4335
                        $last = count($preArray) - 1;
4336
                        $answer = '';
4337
                        for ($k = 0; $k < $last; $k++) {
4338
                            $answer .= $preArray[$k];
4339
                        }
4340
                        $answerWeighting = [$answerWeighting];
4341
                        // we save the answer because it will be modified
4342
                        $temp = $answer;
4343
                        $answer = '';
4344
                        $j = 0;
4345
                        // initialise answer tags
4346
                        $userTags = $correctTags = $realText = [];
4347
                        // the loop will stop at the end of the text
4348
                        while (1) {
4349
                            // quits the loop if there are no more blanks (detect '[')
4350
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4351
                                // adds the end of the text
4352
                                $answer = $temp;
4353
                                $realText[] = $answer;
4354
4355
                                break; //no more "blanks", quit the loop
4356
                            }
4357
                            // adds the piece of text that is before the blank
4358
                            // and ends with '[' into a general storage array
4359
                            $realText[] = api_substr($temp, 0, $pos + 1);
4360
                            $answer .= api_substr($temp, 0, $pos + 1);
4361
                            // take the string remaining (after the last "[" we found)
4362
                            $temp = api_substr($temp, $pos + 1);
4363
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4364
                            if (false === ($pos = api_strpos($temp, ']'))) {
4365
                                // adds the end of the text
4366
                                $answer .= $temp;
4367
4368
                                break;
4369
                            }
4370
4371
                            if ($from_database) {
4372
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4373
                                        WHERE
4374
                                            exe_id = $exeId AND
4375
                                            question_id = $questionId ";
4376
                                $result = Database::query($sql);
4377
                                $str = Database::result($result, 0, 'answer');
4378
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4379
                                $str = str_replace('\r\n', '', $str);
4380
                                $choice = $arr[1];
4381
                                if (isset($choice[$j])) {
4382
                                    $tmp = api_strrpos($choice[$j], ' / ');
4383
                                    if ($tmp) {
4384
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4385
                                    } else {
4386
                                        $tmp = ltrim($tmp, '[');
4387
                                        $tmp = rtrim($tmp, ']');
4388
                                    }
4389
                                    $choice[$j] = trim($choice[$j]);
4390
                                    // Needed to let characters ' and " to work as part of an answer
4391
                                    $choice[$j] = stripslashes($choice[$j]);
4392
                                } else {
4393
                                    $choice[$j] = null;
4394
                                }
4395
                            } else {
4396
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4397
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4398
                            }
4399
                            $userTags[] = $choice[$j];
4400
                            // put the contents of the [] answer tag into correct_tags[]
4401
                            $correctTags[] = api_substr($temp, 0, $pos);
4402
                            $j++;
4403
                            $temp = api_substr($temp, $pos + 1);
4404
                        }
4405
                        $answer = '';
4406
                        $realCorrectTags = $correctTags;
4407
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4408
                        $expectedAnswer = '';
4409
                        $calculatedChoice = '';
4410
4411
                        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...
4412
                            if (0 == $i) {
4413
                                $answer .= $realText[0];
4414
                            }
4415
                            // Needed to parse ' and " characters
4416
                            $userTags[$i] = stripslashes($userTags[$i]);
4417
                            if ($correctTags[$i] == $userTags[$i]) {
4418
                                // gives the related weighting to the student
4419
                                $questionScore += $answerWeighting[$i];
4420
                                // increments total score
4421
                                $totalScore += $answerWeighting[$i];
4422
                                // adds the word in green at the end of the string
4423
                                $answer .= $correctTags[$i];
4424
                                $calculatedChoice = $correctTags[$i];
4425
                            } elseif (!empty($userTags[$i])) {
4426
                                // else if the word entered by the student IS NOT the same as
4427
                                // the one defined by the professor
4428
                                // adds the word in red at the end of the string, and strikes it
4429
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4430
                                $calculatedChoice = $userTags[$i];
4431
                            } else {
4432
                                // adds a tabulation if no word has been typed by the student
4433
                                $answer .= ''; // remove &nbsp; that causes issue
4434
                            }
4435
                            // adds the correct word, followed by ] to close the blank
4436
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4437
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4438
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4439
                                $expectedAnswer = $realCorrectTags[$i];
4440
                            }
4441
                            $answer .= ']';
4442
                            if (isset($realText[$i + 1])) {
4443
                                $answer .= $realText[$i + 1];
4444
                            }
4445
                        }
4446
                    } else {
4447
                        if ($from_database) {
4448
                            $sql = "SELECT *
4449
                                    FROM $TBL_TRACK_ATTEMPT
4450
                                    WHERE
4451
                                        exe_id = $exeId AND
4452
                                        question_id = $questionId ";
4453
                            $result = Database::query($sql);
4454
                            $resultData = Database::fetch_assoc($result);
4455
                            $answer = $resultData['answer'];
4456
                            $questionScore = $resultData['marks'];
4457
                        }
4458
                    }
4459
4460
                    break;
4461
                case FREE_ANSWER:
4462
                    if ($from_database) {
4463
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4464
                                 WHERE
4465
                                    exe_id = $exeId AND
4466
                                    question_id= ".$questionId;
4467
                        $result = Database::query($sql);
4468
                        $data = Database::fetch_array($result);
4469
                        $choice = '';
4470
                        $questionScore = 0;
4471
                        if ($data) {
4472
                            $choice = $data['answer'];
4473
                            $questionScore = $data['marks'];
4474
                        }
4475
4476
                        $choice = str_replace('\r\n', '', $choice);
4477
                        $choice = stripslashes($choice);
4478
4479
                        if (-1 == $questionScore) {
4480
                            $totalScore += 0;
4481
                        } else {
4482
                            $totalScore += $questionScore;
4483
                        }
4484
                        if ('' == $questionScore) {
4485
                            $questionScore = 0;
4486
                        }
4487
                        $arrques = $questionName;
4488
                        $arrans = $choice;
4489
                    } else {
4490
                        $studentChoice = $choice;
4491
                        if ($studentChoice) {
4492
                            //Fixing negative puntation see #2193
4493
                            $questionScore = 0;
4494
                            $totalScore += 0;
4495
                        }
4496
                    }
4497
4498
                    break;
4499
                case ORAL_EXPRESSION:
4500
                    if ($from_database) {
4501
                        $query = "SELECT answer, marks
4502
                                  FROM $TBL_TRACK_ATTEMPT
4503
                                  WHERE
4504
                                        exe_id = $exeId AND
4505
                                        question_id = $questionId
4506
                                 ";
4507
                        $resq = Database::query($query);
4508
                        $row = Database::fetch_assoc($resq);
4509
                        $choice = '';
4510
                        $questionScore = 0;
4511
4512
                        if (is_array($row)) {
4513
                            $choice = $row['answer'];
4514
                            $choice = str_replace('\r\n', '', $choice);
4515
                            $choice = stripslashes($choice);
4516
                            $questionScore = $row['marks'];
4517
                        }
4518
4519
                        if (-1 == $questionScore) {
4520
                            $totalScore += 0;
4521
                        } else {
4522
                            $totalScore += $questionScore;
4523
                        }
4524
                        $arrques = $questionName;
4525
                        $arrans = $choice;
4526
                    } else {
4527
                        $studentChoice = $choice;
4528
                        if ($studentChoice) {
4529
                            //Fixing negative puntation see #2193
4530
                            $questionScore = 0;
4531
                            $totalScore += 0;
4532
                        }
4533
                    }
4534
4535
                    break;
4536
                case MULTIPLE_ANSWER_DROPDOWN:
4537
                    $totalScore += $answerWeighting;
4538
                    break;
4539
                case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
4540
                    break;
4541
                case MATCHING_COMBINATION:
4542
                case MATCHING_DRAGGABLE_COMBINATION:
4543
                case DRAGGABLE:
4544
                case MATCHING_DRAGGABLE:
4545
                case MATCHING:
4546
                    if ($from_database) {
4547
                        $sql = "SELECT iid, answer
4548
                                FROM $table_ans
4549
                                WHERE
4550
                                    question_id = $questionId AND
4551
                                    correct = 0
4552
                                ";
4553
                        $result = Database::query($sql);
4554
                        // Getting the real answer
4555
                        $real_list = [];
4556
                        while ($realAnswer = Database::fetch_array($result)) {
4557
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4558
                        }
4559
4560
                        $orderBy = ' ORDER BY iid ';
4561
                        if (DRAGGABLE == $answerType) {
4562
                            $orderBy = ' ORDER BY correct ';
4563
                        }
4564
4565
                        $sql = "SELECT iid, answer, correct, ponderation
4566
                                FROM $table_ans
4567
                                WHERE
4568
                                    question_id = $questionId AND
4569
                                    correct <> 0
4570
                                $orderBy";
4571
                        $result = Database::query($sql);
4572
                        $options = [];
4573
                        $correctAnswers = [];
4574
                        while ($row = Database::fetch_assoc($result)) {
4575
                            $options[] = $row;
4576
                            $correctAnswers[$row['correct']] = $row['answer'];
4577
                        }
4578
4579
                        $questionScore = 0;
4580
                        $counterAnswer = 1;
4581
                        foreach ($options as $a_answers) {
4582
                            $i_answer_id = $a_answers['iid']; //3
4583
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4584
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4585
                            $i_answer_id_auto = $a_answers['iid']; // 3 - 4
4586
4587
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4588
                                    WHERE
4589
                                        exe_id = '$exeId' AND
4590
                                        question_id = '$questionId' AND
4591
                                        position = '$i_answer_id_auto'";
4592
                            $result = Database::query($sql);
4593
                            $s_user_answer = 0;
4594
                            if (Database::num_rows($result) > 0) {
4595
                                //  rich - good looking
4596
                                $s_user_answer = Database::result($result, 0, 0);
4597
                            }
4598
                            $i_answerWeighting = $a_answers['ponderation'];
4599
                            $user_answer = '';
4600
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4601
4602
                            if (!empty($s_user_answer)) {
4603
                                if (DRAGGABLE == $answerType) {
4604
                                    if ($s_user_answer == $i_answer_correct_answer) {
4605
                                        $questionScore += $i_answerWeighting;
4606
                                        $totalScore += $i_answerWeighting;
4607
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4608
                                        if ($this->showExpectedChoice() && !empty($i_answer_id_auto)) {
4609
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4610
                                        }
4611
                                        $status = Display::label(get_lang('Correct'), 'success');
4612
                                    } else {
4613
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4614
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4615
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4616
                                            $user_answer = $data['answer'];*/
4617
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4618
                                        }
4619
                                    }
4620
                                } else {
4621
                                    if ($s_user_answer == $i_answer_correct_answer) {
4622
                                        $questionScore += $i_answerWeighting;
4623
                                        $totalScore += $i_answerWeighting;
4624
                                        $status = Display::label(get_lang('Correct'), 'success');
4625
4626
                                        // Try with id
4627
                                        if (isset($real_list[$i_answer_id])) {
4628
                                            $user_answer = Display::span(
4629
                                                $real_list[$i_answer_id],
4630
                                                ['style' => 'color: #008000; font-weight: bold;']
4631
                                            );
4632
                                        }
4633
4634
                                        // Try with $i_answer_id_auto
4635
                                        if (empty($user_answer)) {
4636
                                            if (isset($real_list[$i_answer_id_auto])) {
4637
                                                $user_answer = Display::span(
4638
                                                    $real_list[$i_answer_id_auto],
4639
                                                    ['style' => 'color: #008000; font-weight: bold;']
4640
                                                );
4641
                                            }
4642
                                        }
4643
4644
                                        if (isset($real_list[$i_answer_correct_answer])) {
4645
                                            $matchingCorrectAnswers[$questionId]['from_database']['correct'][$i_answer_correct_answer] = $real_list[$i_answer_correct_answer];
4646
                                            $user_answer = Display::span(
4647
                                                $real_list[$i_answer_correct_answer],
4648
                                                ['style' => 'color: #008000; font-weight: bold;']
4649
                                            );
4650
                                        }
4651
                                    } else {
4652
                                        $user_answer = Display::span(
4653
                                            $real_list[$s_user_answer],
4654
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4655
                                        );
4656
                                        if ($this->showExpectedChoice()) {
4657
                                            if (isset($real_list[$s_user_answer])) {
4658
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4659
                                            }
4660
                                        }
4661
                                    }
4662
                                }
4663
                            } elseif (DRAGGABLE == $answerType) {
4664
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4665
                                if ($this->showExpectedChoice()) {
4666
                                    $user_answer = '';
4667
                                }
4668
                            } else {
4669
                                $user_answer = Display::span(
4670
                                    get_lang('Incorrect').' &nbsp;',
4671
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4672
                                );
4673
                                if ($this->showExpectedChoice()) {
4674
                                    $user_answer = '';
4675
                                }
4676
                            }
4677
4678
                            if ($show_result) {
4679
                                if (false === $this->showExpectedChoice() &&
4680
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4681
                                ) {
4682
                                    $user_answer = '';
4683
                                }
4684
                                switch ($answerType) {
4685
                                    case MATCHING:
4686
                                    case MATCHING_DRAGGABLE:
4687
                                    case MATCHING_COMBINATION:
4688
                                    case MATCHING_DRAGGABLE_COMBINATION:
4689
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4690
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4691
                                                break;
4692
                                            }
4693
                                        }
4694
                                        echo '<tr>';
4695
                                        if (!in_array(
4696
                                            $this->results_disabled,
4697
                                            [
4698
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4699
                                            ]
4700
                                        )
4701
                                        ) {
4702
                                            echo '<td>'.$s_answer_label.'</td>';
4703
                                            echo '<td>'.$user_answer.'</td>';
4704
                                        } else {
4705
                                            echo '<td>'.$s_answer_label.'</td>';
4706
                                            $status = Display::label(get_lang('Correct'), 'success');
4707
                                        }
4708
4709
                                        if ($this->showExpectedChoice()) {
4710
                                            if ($this->showExpectedChoiceColumn()) {
4711
                                                echo '<td>';
4712
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4713
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4714
                                                        true == $showTotalScoreAndUserChoicesInLastAttempt
4715
                                                    ) {
4716
                                                        echo Display::span(
4717
                                                            $real_list[$i_answer_correct_answer]
4718
                                                        );
4719
                                                    }
4720
                                                }
4721
                                                echo '</td>';
4722
                                            }
4723
                                            echo '<td class="text-center">'.$status.'</td>';
4724
                                        } else {
4725
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4726
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4727
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4728
                                                ) {
4729
                                                    if ($this->showExpectedChoiceColumn()) {
4730
                                                        echo '<td>';
4731
                                                        echo Display::span(
4732
                                                            $real_list[$i_answer_correct_answer],
4733
                                                            ['style' => 'color: #008000; font-weight: bold;']
4734
                                                        );
4735
                                                        echo '</td>';
4736
                                                    }
4737
                                                }
4738
                                            }
4739
                                        }
4740
                                        echo '</tr>';
4741
4742
                                        break;
4743
                                    case DRAGGABLE:
4744
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4745
                                            $s_answer_label = '';
4746
                                        }
4747
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4748
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4749
                                                break;
4750
                                            }
4751
                                        }
4752
                                        echo '<tr>';
4753
                                        if ($this->showExpectedChoice()) {
4754
                                            if (!in_array(
4755
                                                $this->results_disabled,
4756
                                                [
4757
                                                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4758
                                                    //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4759
                                                ]
4760
                                            )
4761
                                            ) {
4762
                                                echo '<td>'.$user_answer.'</td>';
4763
                                            } else {
4764
                                                $status = Display::label(get_lang('Correct'), 'success');
4765
                                            }
4766
                                            echo '<td>'.$s_answer_label.'</td>';
4767
                                            echo '<td class="text-center">'.$status.'</td>';
4768
                                        } else {
4769
                                            echo '<td>'.$s_answer_label.'</td>';
4770
                                            echo '<td>'.$user_answer.'</td>';
4771
                                            echo '<td>';
4772
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4773
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4774
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4775
                                                ) {
4776
                                                    echo Display::span(
4777
                                                        $real_list[$i_answer_correct_answer],
4778
                                                        ['style' => 'color: #008000; font-weight: bold;']
4779
                                                    );
4780
                                                }
4781
                                            }
4782
                                            echo '</td>';
4783
                                        }
4784
                                        echo '</tr>';
4785
4786
                                        break;
4787
                                }
4788
                            }
4789
                            $counterAnswer++;
4790
                        }
4791
                        $matchingCorrectAnswers[$questionId]['from_database']['count_options'] = count($options);
4792
                        break 2; // break the switch and the "for" condition
4793
                    } else {
4794
                        if ($answerCorrect) {
4795
                            if (isset($choice[$answerAutoId]) &&
4796
                                $answerCorrect == $choice[$answerAutoId]
4797
                            ) {
4798
                                $matchingCorrectAnswers[$questionId]['form_values']['correct'][$answerAutoId] = $choice[$answerAutoId];
4799
                                $correctAnswerId[] = $answerAutoId;
4800
                                $questionScore += $answerWeighting;
4801
                                $totalScore += $answerWeighting;
4802
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4803
                            } else {
4804
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4805
                                    $user_answer = Display::span(
4806
                                        $answerMatching[$choice[$answerAutoId]],
4807
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4808
                                    );
4809
                                }
4810
                            }
4811
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4812
                        }
4813
                        $matchingCorrectAnswers[$questionId]['form_values']['count_options'] = count($choice);
4814
                    }
4815
4816
                    break;
4817
                case HOT_SPOT:
4818
                case HOT_SPOT_COMBINATION:
4819
                    if ($from_database) {
4820
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4821
                        // Check auto id
4822
                        $foundAnswerId = $answerAutoId;
4823
                        $sql = "SELECT hotspot_correct
4824
                                FROM $TBL_TRACK_HOTSPOT
4825
                                WHERE
4826
                                    hotspot_exe_id = $exeId AND
4827
                                    hotspot_question_id= $questionId AND
4828
                                    hotspot_answer_id = $answerAutoId
4829
                                ORDER BY hotspot_id ASC";
4830
                        $result = Database::query($sql);
4831
                        if (Database::num_rows($result)) {
4832
                            $studentChoice = Database::result(
4833
                                $result,
4834
                                0,
4835
                                'hotspot_correct'
4836
                            );
4837
4838
                            if ($studentChoice) {
4839
                                $questionScore += $answerWeighting;
4840
                                $totalScore += $answerWeighting;
4841
                            }
4842
                        } else {
4843
                            // If answer.id is different:
4844
                            $sql = "SELECT hotspot_correct
4845
                                FROM $TBL_TRACK_HOTSPOT
4846
                                WHERE
4847
                                    hotspot_exe_id = $exeId AND
4848
                                    hotspot_question_id= $questionId AND
4849
                                    hotspot_answer_id = ".(int) $answerId.'
4850
                                ORDER BY hotspot_id ASC';
4851
                            $result = Database::query($sql);
4852
4853
                            $foundAnswerId = $answerId;
4854
                            if (Database::num_rows($result)) {
4855
                                $studentChoice = Database::result(
4856
                                    $result,
4857
                                    0,
4858
                                    'hotspot_correct'
4859
                                );
4860
4861
                                if ($studentChoice) {
4862
                                    $questionScore += $answerWeighting;
4863
                                    $totalScore += $answerWeighting;
4864
                                }
4865
                            } else {
4866
                                // check answer.iid
4867
                                if (!empty($answerIid)) {
4868
                                    $sql = "SELECT hotspot_correct
4869
                                            FROM $TBL_TRACK_HOTSPOT
4870
                                            WHERE
4871
                                                hotspot_exe_id = $exeId AND
4872
                                                hotspot_question_id= $questionId AND
4873
                                                hotspot_answer_id = $answerIid
4874
                                            ORDER BY hotspot_id ASC";
4875
                                    $result = Database::query($sql);
4876
4877
                                    $foundAnswerId = $answerIid;
4878
                                    $studentChoice = Database::result(
4879
                                        $result,
4880
                                        0,
4881
                                        'hotspot_correct'
4882
                                    );
4883
4884
                                    if ($studentChoice) {
4885
                                        $questionScore += $answerWeighting;
4886
                                        $totalScore += $answerWeighting;
4887
                                    }
4888
                                }
4889
                            }
4890
                        }
4891
                    } else {
4892
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4893
                            $choice[$answerAutoId] = 0;
4894
                            $choice[$answerIid] = 0;
4895
                        } else {
4896
                            $studentChoice = $choice[$answerAutoId];
4897
                            if (empty($studentChoice)) {
4898
                                $studentChoice = $choice[$answerIid];
4899
                            }
4900
                            $choiceIsValid = false;
4901
                            if (!empty($studentChoice)) {
4902
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4903
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4904
                                $choicePoint = Geometry::decodePoint($studentChoice);
4905
4906
                                switch ($hotspotType) {
4907
                                    case 'square':
4908
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4909
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4910
4911
                                        break;
4912
                                    case 'circle':
4913
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4914
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4915
4916
                                        break;
4917
                                    case 'poly':
4918
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4919
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4920
4921
                                        break;
4922
                                }
4923
                            }
4924
4925
                            $choice[$answerAutoId] = 0;
4926
                            if ($choiceIsValid) {
4927
                                $questionScore += $answerWeighting;
4928
                                $totalScore += $answerWeighting;
4929
                                $choice[$answerAutoId] = 1;
4930
                                $choice[$answerIid] = 1;
4931
                            }
4932
                            $studentChoice = $choiceIsValid ? 1 : 0;
4933
                        }
4934
                    }
4935
4936
                    break;
4937
                case HOT_SPOT_ORDER:
4938
                    // @todo never added to chamilo
4939
                    // for hotspot with fixed order
4940
                    $studentChoice = $choice['order'][$answerId];
4941
                    if ($studentChoice == $answerId) {
4942
                        $questionScore += $answerWeighting;
4943
                        $totalScore += $answerWeighting;
4944
                        $studentChoice = true;
4945
                    } else {
4946
                        $studentChoice = false;
4947
                    }
4948
4949
                    break;
4950
                case HOT_SPOT_DELINEATION:
4951
                    // for hotspot with delineation
4952
                    if ($from_database) {
4953
                        // getting the user answer
4954
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4955
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4956
                                    FROM $TBL_TRACK_HOTSPOT
4957
                                    WHERE
4958
                                        hotspot_exe_id = $exeId AND
4959
                                        hotspot_question_id= $questionId AND
4960
                                        hotspot_answer_id = '1'";
4961
                        // By default we take 1 because it's a delineation
4962
                        $resq = Database::query($query);
4963
                        $row = Database::fetch_assoc($resq);
4964
4965
                        if ($row && isset($row['hotspot_correct'], $row['hotspot_coordinate'])) {
4966
                            $choice = $row['hotspot_correct'];
4967
                            $user_answer = $row['hotspot_coordinate'];
4968
                            $coords = explode('/', $user_answer);
4969
                        } else {
4970
                            $choice = '';
4971
                            $user_answer = '';
4972
                            $coords = [];
4973
                        }
4974
4975
                        $user_array = '';
4976
                        foreach ($coords as $coord) {
4977
                            [$x, $y] = explode(';', $coord);
4978
                            $user_array .= round($x).';'.round($y).'/';
4979
                        }
4980
                        $user_array = substr($user_array, 0, -1) ?: '';
4981
                    } else {
4982
                        if (!empty($studentChoice)) {
4983
                            $correctAnswerId[] = $answerAutoId;
4984
                            $newquestionList[] = $questionId;
4985
                        }
4986
4987
                        if (1 === $answerId && isset($choice[$answerId])) {
4988
                            $studentChoice = $choice[$answerId];
4989
                            $questionScore += $answerWeighting;
4990
                        }
4991
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4992
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4993
                        }
4994
                    }
4995
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4996
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4997
4998
                    break;
4999
                case FILL_IN_BLANKS_COMBINATION:
5000
                        $answerFromDatabase = '';
5001
                        if ($from_database) {
5002
                            $sql = "SELECT answer
5003
                            FROM $TBL_TRACK_ATTEMPT
5004
                            WHERE exe_id = $exeId AND question_id = $questionId";
5005
                            $result = Database::query($sql);
5006
                            $answerFromDatabase = Database::result($result, 0, 'answer');
5007
                        }
5008
5009
                        // Teacher definition and weights
5010
                        $info = FillBlanks::getAnswerInfo($answer);
5011
                        $listCorrectAnswers = $info; // keep words, weighting, switchable
5012
                        $switchableAnswerSet = $info['switchable'];
5013
                        $answerWeighting = $info['weighting'];
5014
5015
                        // Student choices
5016
                        if ($from_database && $answerFromDatabase !== '') {
5017
                            $studentInfo = FillBlanks::getAnswerInfo($answerFromDatabase, true);
5018
                            $choice = $studentInfo['student_answer'] ?? [];
5019
                        }
5020
5021
                        // Evaluate each blank and fill student_answer/student_score only
5022
                        if (!$switchableAnswerSet) {
5023
                            // Fixed positions
5024
                            $count = count($info['words']);
5025
                            for ($i = 0; $i < $count; $i++) {
5026
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
5027
                                if (!$from_database) {
5028
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
5029
                                }
5030
                                $correctAnswer = $info['words'][$i];
5031
5032
                                // Resolve menu placeholders when present (for display)
5033
                                $studentAnswerToShow = $studentAnswer;
5034
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
5035
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type && $studentAnswer !== '') {
5036
                                    $menu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
5037
                                    foreach ($menu as $opt) {
5038
                                        if (sha1($opt) === $studentAnswer) {
5039
                                            $studentAnswerToShow = $opt;
5040
                                            break;
5041
                                        }
5042
                                    }
5043
                                }
5044
5045
                                $ok = FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database);
5046
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
5047
                                $listCorrectAnswers['student_score'][$i]  = $ok ? 1 : 0;
5048
                            }
5049
                        } else {
5050
                            // Switchable answers: any student entry can match any teacher word once
5051
                            $studentTemp = (array) ($choice ?? []);
5052
                            $teacherTemp = $info['words'];
5053
5054
                            $count = max(count($studentTemp), count($teacherTemp));
5055
                            for ($i = 0; $i < $count; $i++) {
5056
                                $studentAnswer = isset($studentTemp[$i]) ? trim($studentTemp[$i]) : '';
5057
                                $studentAnswerToShow = $studentAnswer;
5058
5059
                                if ($studentAnswer !== '') {
5060
                                    if (!$from_database) {
5061
                                        $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
5062
                                    }
5063
                                    $found = false;
5064
                                    foreach ($teacherTemp as $j => $correctAnswer) {
5065
                                        if ($found) {
5066
                                            continue;
5067
                                        }
5068
                                        if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
5069
                                            // consume this teacher slot
5070
                                            $teacherTemp[$j] = '';
5071
                                            $found = true;
5072
                                        }
5073
                                    }
5074
5075
                                    // Resolve menu placeholder for display
5076
                                    if ($studentAnswerToShow !== '') {
5077
                                        $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer ?? '');
5078
                                        if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
5079
                                            $menu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
5080
                                            foreach ($menu as $opt) {
5081
                                                if (sha1($opt) === $studentAnswer) {
5082
                                                    $studentAnswerToShow = $opt;
5083
                                                    break;
5084
                                                }
5085
                                            }
5086
                                        }
5087
                                    }
5088
5089
                                    $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
5090
                                    $listCorrectAnswers['student_score'][$i]  = $found ? 1 : 0;
5091
                                } else {
5092
                                    $listCorrectAnswers['student_answer'][$i] = '';
5093
                                    $listCorrectAnswers['student_score'][$i]  = 0;
5094
                                }
5095
                            }
5096
                        }
5097
5098
                        // Build the annotated answer string for display
5099
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
5100
                    break;
5101
                case HOT_SPOT_COMBINATION:
5102
                        if ($from_database) {
5103
                            $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5104
                            // Try by auto id
5105
                            $sql = "SELECT hotspot_correct
5106
                                FROM $TBL_TRACK_HOTSPOT
5107
                                WHERE hotspot_exe_id = $exeId
5108
                                  AND hotspot_question_id = $questionId
5109
                                  AND hotspot_answer_id = $answerAutoId
5110
                                ORDER BY hotspot_id ASC";
5111
                            $result = Database::query($sql);
5112
                            if (Database::num_rows($result)) {
5113
                                $studentChoice = (int) Database::result($result, 0, 'hotspot_correct');
5114
                                $choice[$answerAutoId] = $studentChoice ? 1 : 0;
5115
                            } else {
5116
                                // Fallback to legacy ids
5117
                                $studentChoice = 0;
5118
                                if (!empty($answerIid)) {
5119
                                    $sql = "SELECT hotspot_correct
5120
                                        FROM $TBL_TRACK_HOTSPOT
5121
                                        WHERE hotspot_exe_id = $exeId
5122
                                          AND hotspot_question_id = $questionId
5123
                                          AND hotspot_answer_id = $answerIid
5124
                                        ORDER BY hotspot_id ASC";
5125
                                    $result = Database::query($sql);
5126
                                    if (Database::num_rows($result)) {
5127
                                        $studentChoice = (int) Database::result($result, 0, 'hotspot_correct');
5128
                                    }
5129
                                }
5130
                                $choice[$answerAutoId] = $studentChoice ? 1 : 0;
5131
                                $choice[$answerIid]    = $choice[$answerAutoId];
5132
                            }
5133
                        } else {
5134
                            // Validate coordinate inside the expected shape and mark $choice
5135
                            if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
5136
                                $choice[$answerAutoId] = 0;
5137
                                $choice[$answerIid]    = 0;
5138
                                $studentChoice = 0;
5139
                            } else {
5140
                                $studentChoicePoint = $choice[$answerAutoId] ?? $choice[$answerIid] ?? '';
5141
                                $isValid = false;
5142
5143
                                if (!empty($studentChoicePoint)) {
5144
                                    $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
5145
                                    $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
5146
                                    $choicePoint = Geometry::decodePoint($studentChoicePoint);
5147
5148
                                    switch ($hotspotType) {
5149
                                        case 'square':
5150
                                            $props = Geometry::decodeSquare($hotspotCoordinates);
5151
                                            $isValid = Geometry::pointIsInSquare($props, $choicePoint);
5152
                                            break;
5153
                                        case 'circle':
5154
                                            $props = Geometry::decodeEllipse($hotspotCoordinates);
5155
                                            $isValid = Geometry::pointIsInEllipse($props, $choicePoint);
5156
                                            break;
5157
                                        case 'poly':
5158
                                            $props = Geometry::decodePolygon($hotspotCoordinates);
5159
                                            $isValid = Geometry::pointIsInPolygon($props, $choicePoint);
5160
                                            break;
5161
                                    }
5162
                                }
5163
5164
                                $choice[$answerAutoId] = $isValid ? 1 : 0;
5165
                                $choice[$answerIid]    = $choice[$answerAutoId];
5166
                                $studentChoice         = $choice[$answerAutoId];
5167
                            }
5168
                        }
5169
                    break;
5170
                case ANNOTATION:
5171
                    if ($from_database) {
5172
                        $sql = "SELECT answer, marks
5173
                                FROM $TBL_TRACK_ATTEMPT
5174
                                WHERE
5175
                                  exe_id = $exeId AND
5176
                                  question_id = $questionId ";
5177
                        $resq = Database::query($sql);
5178
                        $data = Database::fetch_array($resq);
5179
5180
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
5181
                        $arrques = $questionName;
5182
5183
                        break;
5184
                    }
5185
                    $studentChoice = $choice;
5186
                    if ($studentChoice) {
5187
                        $questionScore = 0;
5188
                    }
5189
5190
                    break;
5191
            }
5192
5193
            if ($show_result) {
5194
                if ('exercise_result' === $from) {
5195
                    // Display answers (if not matching type, or if the answer is correct)
5196
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION]) ||
5197
                        $answerCorrect
5198
                    ) {
5199
                        if (in_array(
5200
                            $answerType,
5201
                            [
5202
                                UNIQUE_ANSWER,
5203
                                UNIQUE_ANSWER_IMAGE,
5204
                                UNIQUE_ANSWER_NO_OPTION,
5205
                                MULTIPLE_ANSWER,
5206
                                MULTIPLE_ANSWER_COMBINATION,
5207
                                GLOBAL_MULTIPLE_ANSWER,
5208
                                READING_COMPREHENSION,
5209
                            ]
5210
                        )) {
5211
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
5212
                                $this,
5213
                                $feedback_type,
5214
                                $answerType,
5215
                                $studentChoice,
5216
                                $answer,
5217
                                $answerComment,
5218
                                $answerCorrect,
5219
                                0,
5220
                                0,
5221
                                0,
5222
                                $results_disabled,
5223
                                $showTotalScoreAndUserChoicesInLastAttempt,
5224
                                $this->export
5225
                            );
5226
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
5227
                            ExerciseShowFunctions::display_multiple_answer_true_false(
5228
                                $this,
5229
                                $feedback_type,
5230
                                $answerType,
5231
                                $studentChoice,
5232
                                $answer,
5233
                                $answerComment,
5234
                                $answerCorrect,
5235
                                0,
5236
                                $questionId,
5237
                                0,
5238
                                $results_disabled,
5239
                                $showTotalScoreAndUserChoicesInLastAttempt
5240
                            );
5241
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
5242
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5243
                                $this,
5244
                                $feedback_type,
5245
                                $studentChoice,
5246
                                $studentChoiceDegree,
5247
                                $answer,
5248
                                $answerComment,
5249
                                $answerCorrect,
5250
                                $questionId,
5251
                                $results_disabled
5252
                            );
5253
                        } elseif (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) {
5254
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5255
                                $this,
5256
                                $feedback_type,
5257
                                $answerType,
5258
                                $studentChoice,
5259
                                $answer,
5260
                                $answerComment,
5261
                                $answerCorrect,
5262
                                0,
5263
                                0,
5264
                                0,
5265
                                $results_disabled,
5266
                                $showTotalScoreAndUserChoicesInLastAttempt
5267
                            );
5268
                        } elseif (in_array($answerType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
5269
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5270
                                $this,
5271
                                $feedback_type,
5272
                                $answer,
5273
                                0,
5274
                                0,
5275
                                $results_disabled,
5276
                                $showTotalScoreAndUserChoicesInLastAttempt,
5277
                                ''
5278
                            );
5279
                        } elseif (CALCULATED_ANSWER == $answerType) {
5280
                            ExerciseShowFunctions::display_calculated_answer(
5281
                                $this,
5282
                                $feedback_type,
5283
                                $answer,
5284
                                0,
5285
                                0,
5286
                                $results_disabled,
5287
                                $showTotalScoreAndUserChoicesInLastAttempt,
5288
                                $expectedAnswer,
5289
                                $calculatedChoice,
5290
                                $calculatedStatus
5291
                            );
5292
                        } elseif (FREE_ANSWER == $answerType) {
5293
                            ExerciseShowFunctions::display_free_answer(
5294
                                $feedback_type,
5295
                                $choice,
5296
                                $exeId,
5297
                                $questionId,
5298
                                $questionScore,
5299
                                $results_disabled
5300
                            );
5301
                        } elseif (ORAL_EXPRESSION == $answerType) {
5302
                            // to store the details of open questions in an array to be used in mail
5303
                            /** @var OralExpression $objQuestionTmp */
5304
                            ExerciseShowFunctions::display_oral_expression_answer(
5305
                                $feedback_type,
5306
                                $choice,
5307
                                $exeId,
5308
                                $questionId,
5309
                                $results_disabled,
5310
                                $questionScore,
5311
                                true
5312
                            );
5313
                        } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION], true)) {
5314
                            $correctAnswerId = 0;
5315
                            /** @var TrackEHotspot $hotspot */
5316
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5317
                                if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
5318
                                    break;
5319
                                }
5320
                            }
5321
5322
                            // force to show whether the choice is correct or not
5323
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5324
                            ExerciseShowFunctions::display_hotspot_answer(
5325
                                $this,
5326
                                $feedback_type,
5327
                                $answerId,
5328
                                $answer,
5329
                                $studentChoice,
5330
                                $answerComment,
5331
                                $results_disabled,
5332
                                $answerId,
5333
                                $showTotalScoreAndUserChoicesInLastAttempt
5334
                            );
5335
                        } elseif (HOT_SPOT_ORDER == $answerType) {
5336
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5337
                                $feedback_type,
5338
                                $answerId,
5339
                                $answer,
5340
                                $studentChoice,
5341
                                $answerComment
5342
                            );*/
5343
                        } elseif (HOT_SPOT_DELINEATION == $answerType && isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5344
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5345
5346
                            // Round-up the coordinates
5347
                            $coords = explode('/', $user_answer);
5348
                            $coords = array_filter($coords);
5349
                            $user_array = '';
5350
                            foreach ($coords as $coord) {
5351
                                if (!empty($coord)) {
5352
                                    $parts = explode(';', $coord);
5353
                                    if (!empty($parts)) {
5354
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5355
                                    }
5356
                                }
5357
                            }
5358
                            $user_array = substr($user_array, 0, -1) ?: '';
5359
                            if ($next) {
5360
                                $user_answer = $user_array;
5361
                                // We compare only the delineation not the other points
5362
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5363
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5364
5365
                                // Calculating the area
5366
                                $poly_user = convert_coordinates($user_answer, '/');
5367
                                $poly_answer = convert_coordinates($answer_question, '|');
5368
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5369
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5370
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5371
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5372
5373
                                $overlap = $poly_results['both'];
5374
                                $poly_answer_area = $poly_results['s1'];
5375
                                $poly_user_area = $poly_results['s2'];
5376
                                $missing = $poly_results['s1Only'];
5377
                                $excess = $poly_results['s2Only'];
5378
5379
                                // //this is an area in pixels
5380
                                if ($debug > 0) {
5381
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5382
                                }
5383
5384
                                if ($overlap < 1) {
5385
                                    // Shortcut to avoid complicated calculations
5386
                                    $final_overlap = 0;
5387
                                    $final_missing = 100;
5388
                                    $final_excess = 100;
5389
                                } else {
5390
                                    // the final overlap is the percentage of the initial polygon
5391
                                    // that is overlapped by the user's polygon
5392
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5393
                                    if ($debug > 1) {
5394
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5395
                                    }
5396
                                    // the final missing area is the percentage of the initial polygon
5397
                                    // that is not overlapped by the user's polygon
5398
                                    $final_missing = 100 - $final_overlap;
5399
                                    if ($debug > 1) {
5400
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5401
                                    }
5402
                                    // the final excess area is the percentage of the initial polygon's size
5403
                                    // that is covered by the user's polygon outside of the initial polygon
5404
                                    $final_excess = round(
5405
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5406
                                    );
5407
                                    if ($debug > 1) {
5408
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5409
                                    }
5410
                                }
5411
5412
                                // Checking the destination parameters parsing the "@@"
5413
                                $destination_items = explode('@@', $answerDestination);
5414
                                $threadhold_total = $destination_items[0];
5415
                                $threadhold_items = explode(';', $threadhold_total);
5416
                                $threadhold1 = $threadhold_items[0]; // overlap
5417
                                $threadhold2 = $threadhold_items[1]; // excess
5418
                                $threadhold3 = $threadhold_items[2]; // missing
5419
5420
                                // if is delineation
5421
                                if (1 === $answerId) {
5422
                                    //setting colors
5423
                                    if ($final_overlap >= $threadhold1) {
5424
                                        $overlap_color = true;
5425
                                    }
5426
                                    if ($final_excess <= $threadhold2) {
5427
                                        $excess_color = true;
5428
                                    }
5429
                                    if ($final_missing <= $threadhold3) {
5430
                                        $missing_color = true;
5431
                                    }
5432
5433
                                    // if pass
5434
                                    if ($final_overlap >= $threadhold1 &&
5435
                                        $final_missing <= $threadhold3 &&
5436
                                        $final_excess <= $threadhold2
5437
                                    ) {
5438
                                        $next = 1; //go to the oars
5439
                                        $result_comment = get_lang('Acceptable');
5440
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5441
                                    } else {
5442
                                        $next = 0;
5443
                                        $result_comment = get_lang('Unacceptable');
5444
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5445
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5446
                                        // checking the destination parameters parsing the "@@"
5447
                                        $destination_items = explode('@@', $answerDestination);
5448
                                    }
5449
                                } elseif ($answerId > 1) {
5450
                                    if ('noerror' == $objAnswerTmp->selectHotspotType($answerId)) {
5451
                                        if ($debug > 0) {
5452
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5453
                                        }
5454
                                        //type no error shouldn't be treated
5455
                                        $next = 1;
5456
5457
                                        continue;
5458
                                    }
5459
                                    if ($debug > 0) {
5460
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5461
                                    }
5462
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5463
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5464
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5465
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5466
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5467
5468
                                    if (false == $overlap) {
5469
                                        //all good, no overlap
5470
                                        $next = 1;
5471
5472
                                        continue;
5473
                                    } else {
5474
                                        if ($debug > 0) {
5475
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5476
                                        }
5477
                                        $organs_at_risk_hit++;
5478
                                        //show the feedback
5479
                                        $next = 0;
5480
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5481
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5482
5483
                                        $destination_items = explode('@@', $answerDestination);
5484
                                        $try_hotspot = $destination_items[1];
5485
                                        $lp_hotspot = $destination_items[2];
5486
                                        $select_question_hotspot = $destination_items[3];
5487
                                        $url_hotspot = $destination_items[4];
5488
                                    }
5489
                                }
5490
                            } else {
5491
                                // the first delineation feedback
5492
                                if ($debug > 0) {
5493
                                    error_log(__LINE__.' first', 0);
5494
                                }
5495
                            }
5496
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
5497
                            echo '<tr>';
5498
                            echo Display::tag('td', $answerMatching[$answerId]);
5499
                            echo Display::tag(
5500
                                'td',
5501
                                "$user_answer / ".Display::tag(
5502
                                    'strong',
5503
                                    $answerMatching[$answerCorrect],
5504
                                    ['style' => 'color: #008000; font-weight: bold;']
5505
                                )
5506
                            );
5507
                            echo '</tr>';
5508
                        } elseif (ANNOTATION == $answerType) {
5509
                            ExerciseShowFunctions::displayAnnotationAnswer(
5510
                                $feedback_type,
5511
                                $exeId,
5512
                                $questionId,
5513
                                $questionScore,
5514
                                $results_disabled
5515
                            );
5516
                        }
5517
                    }
5518
                } else {
5519
                    if ($debug) {
5520
                        error_log('Showing questions $from '.$from);
5521
                    }
5522
5523
                    switch ($answerType) {
5524
                        case UNIQUE_ANSWER:
5525
                        case UNIQUE_ANSWER_IMAGE:
5526
                        case UNIQUE_ANSWER_NO_OPTION:
5527
                        case MULTIPLE_ANSWER:
5528
                        case GLOBAL_MULTIPLE_ANSWER:
5529
                        case MULTIPLE_ANSWER_COMBINATION:
5530
                        case READING_COMPREHENSION:
5531
                            if (1 == $answerId) {
5532
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5533
                                    $this,
5534
                                    $feedback_type,
5535
                                    $answerType,
5536
                                    $studentChoice,
5537
                                    $answer,
5538
                                    $answerComment,
5539
                                    $answerCorrect,
5540
                                    $exeId,
5541
                                    $questionId,
5542
                                    $answerId,
5543
                                    $results_disabled,
5544
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5545
                                    $this->export
5546
                                );
5547
                            } else {
5548
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5549
                                    $this,
5550
                                    $feedback_type,
5551
                                    $answerType,
5552
                                    $studentChoice,
5553
                                    $answer,
5554
                                    $answerComment,
5555
                                    $answerCorrect,
5556
                                    $exeId,
5557
                                    $questionId,
5558
                                    '',
5559
                                    $results_disabled,
5560
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5561
                                    $this->export
5562
                                );
5563
                            }
5564
5565
                            break;
5566
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5567
                            if (1 == $answerId) {
5568
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5569
                                    $this,
5570
                                    $feedback_type,
5571
                                    $answerType,
5572
                                    $studentChoice,
5573
                                    $answer,
5574
                                    $answerComment,
5575
                                    $answerCorrect,
5576
                                    $exeId,
5577
                                    $questionId,
5578
                                    $answerId,
5579
                                    $results_disabled,
5580
                                    $showTotalScoreAndUserChoicesInLastAttempt
5581
                                );
5582
                            } else {
5583
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5584
                                    $this,
5585
                                    $feedback_type,
5586
                                    $answerType,
5587
                                    $studentChoice,
5588
                                    $answer,
5589
                                    $answerComment,
5590
                                    $answerCorrect,
5591
                                    $exeId,
5592
                                    $questionId,
5593
                                    '',
5594
                                    $results_disabled,
5595
                                    $showTotalScoreAndUserChoicesInLastAttempt
5596
                                );
5597
                            }
5598
5599
                            break;
5600
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5601
                            if (1 == $answerId) {
5602
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5603
                                    $this,
5604
                                    $feedback_type,
5605
                                    $answerType,
5606
                                    $studentChoice,
5607
                                    $answer,
5608
                                    $answerComment,
5609
                                    $answerCorrect,
5610
                                    $exeId,
5611
                                    $questionId,
5612
                                    $answerId,
5613
                                    $results_disabled,
5614
                                    $showTotalScoreAndUserChoicesInLastAttempt
5615
                                );
5616
                            } else {
5617
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5618
                                    $this,
5619
                                    $feedback_type,
5620
                                    $answerType,
5621
                                    $studentChoice,
5622
                                    $answer,
5623
                                    $answerComment,
5624
                                    $answerCorrect,
5625
                                    $exeId,
5626
                                    $questionId,
5627
                                    '',
5628
                                    $results_disabled,
5629
                                    $showTotalScoreAndUserChoicesInLastAttempt
5630
                                );
5631
                            }
5632
5633
                            break;
5634
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5635
                            if (1 == $answerId) {
5636
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5637
                                    $this,
5638
                                    $feedback_type,
5639
                                    $studentChoice,
5640
                                    $studentChoiceDegree,
5641
                                    $answer,
5642
                                    $answerComment,
5643
                                    $answerCorrect,
5644
                                    $questionId,
5645
                                    $results_disabled
5646
                                );
5647
                            } else {
5648
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5649
                                    $this,
5650
                                    $feedback_type,
5651
                                    $studentChoice,
5652
                                    $studentChoiceDegree,
5653
                                    $answer,
5654
                                    $answerComment,
5655
                                    $answerCorrect,
5656
                                    $questionId,
5657
                                    $results_disabled
5658
                                );
5659
                            }
5660
5661
                            break;
5662
                        case FILL_IN_BLANKS:
5663
                        case FILL_IN_BLANKS_COMBINATION:
5664
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5665
                                $this,
5666
                                $feedback_type,
5667
                                $answer,
5668
                                $exeId,
5669
                                $questionId,
5670
                                $results_disabled,
5671
                                $showTotalScoreAndUserChoicesInLastAttempt,
5672
                                $str
5673
                            );
5674
                            break;
5675
                        case CALCULATED_ANSWER:
5676
                            ExerciseShowFunctions::display_calculated_answer(
5677
                                $this,
5678
                                $feedback_type,
5679
                                $answer,
5680
                                $exeId,
5681
                                $questionId,
5682
                                $results_disabled,
5683
                                '',
5684
                                $showTotalScoreAndUserChoicesInLastAttempt
5685
                            );
5686
5687
                            break;
5688
                        case FREE_ANSWER:
5689
                            echo ExerciseShowFunctions::display_free_answer(
5690
                                $feedback_type,
5691
                                $choice,
5692
                                $exeId,
5693
                                $questionId,
5694
                                $questionScore,
5695
                                $results_disabled
5696
                            );
5697
5698
                            break;
5699
                        case ORAL_EXPRESSION:
5700
                            /** @var OralExpression $objQuestionTmp */
5701
                            echo '<tr>
5702
                                <td valign="top">'.
5703
                                ExerciseShowFunctions::display_oral_expression_answer(
5704
                                    $feedback_type,
5705
                                    $choice,
5706
                                    $exeId,
5707
                                    $questionId,
5708
                                    $results_disabled,
5709
                                    $questionScore
5710
                                ).'</td>
5711
                                </tr>
5712
                                </table>';
5713
                            break;
5714
                        case HOT_SPOT:
5715
                        case HOT_SPOT_COMBINATION:
5716
                            $correctAnswerId = 0;
5717
5718
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5719
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5720
                                    break;
5721
                                }
5722
                            }
5723
                            ExerciseShowFunctions::display_hotspot_answer(
5724
                                $this,
5725
                                $feedback_type,
5726
                                $answerId,
5727
                                $answer,
5728
                                $studentChoice,
5729
                                $answerComment,
5730
                                $results_disabled,
5731
                                $answerId,
5732
                                $showTotalScoreAndUserChoicesInLastAttempt
5733
                            );
5734
5735
                            break;
5736
                        case HOT_SPOT_DELINEATION:
5737
                            $user_answer = $user_array;
5738
                            if ($next) {
5739
                                $user_answer = $user_array;
5740
                                // we compare only the delineation not the other points
5741
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5742
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5743
5744
                                // calculating the area
5745
                                $poly_user = convert_coordinates($user_answer, '/');
5746
                                $poly_answer = convert_coordinates($answer_question, '|');
5747
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5748
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5749
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5750
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5751
5752
                                $overlap = $poly_results['both'];
5753
                                $poly_answer_area = $poly_results['s1'];
5754
                                $poly_user_area = $poly_results['s2'];
5755
                                $missing = $poly_results['s1Only'];
5756
                                $excess = $poly_results['s2Only'];
5757
                                if ($debug > 0) {
5758
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5759
                                }
5760
                                if ($overlap < 1) {
5761
                                    //shortcut to avoid complicated calculations
5762
                                    $final_overlap = 0;
5763
                                    $final_missing = 100;
5764
                                    $final_excess = 100;
5765
                                } else {
5766
                                    // the final overlap is the percentage of the initial polygon
5767
                                    // that is overlapped by the user's polygon
5768
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5769
5770
                                    // the final missing area is the percentage of the initial polygon that
5771
                                    // is not overlapped by the user's polygon
5772
                                    $final_missing = 100 - $final_overlap;
5773
                                    // the final excess area is the percentage of the initial polygon's size that is
5774
                                    // covered by the user's polygon outside of the initial polygon
5775
                                    $final_excess = round(
5776
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5777
                                    );
5778
5779
                                    if ($debug > 1) {
5780
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5781
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5782
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5783
                                    }
5784
                                }
5785
5786
                                // Checking the destination parameters parsing the "@@"
5787
                                $destination_items = explode('@@', $answerDestination);
5788
                                $threadhold_total = $destination_items[0];
5789
                                $threadhold_items = explode(';', $threadhold_total);
5790
                                $threadhold1 = $threadhold_items[0]; // overlap
5791
                                $threadhold2 = $threadhold_items[1]; // excess
5792
                                $threadhold3 = $threadhold_items[2]; //missing
5793
                                // if is delineation
5794
                                if (1 === $answerId) {
5795
                                    //setting colors
5796
                                    if ($final_overlap >= $threadhold1) {
5797
                                        $overlap_color = true;
5798
                                    }
5799
                                    if ($final_excess <= $threadhold2) {
5800
                                        $excess_color = true;
5801
                                    }
5802
                                    if ($final_missing <= $threadhold3) {
5803
                                        $missing_color = true;
5804
                                    }
5805
5806
                                    // if pass
5807
                                    if ($final_overlap >= $threadhold1 &&
5808
                                        $final_missing <= $threadhold3 &&
5809
                                        $final_excess <= $threadhold2
5810
                                    ) {
5811
                                        $next = 1; //go to the oars
5812
                                        $result_comment = get_lang('Acceptable');
5813
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5814
                                    } else {
5815
                                        $next = 0;
5816
                                        $result_comment = get_lang('Unacceptable');
5817
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5818
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5819
                                        //checking the destination parameters parsing the "@@"
5820
                                        $destination_items = explode('@@', $answerDestination);
5821
                                    }
5822
                                } elseif ($answerId > 1) {
5823
                                    if ('noerror' === $objAnswerTmp->selectHotspotType($answerId)) {
5824
                                        if ($debug > 0) {
5825
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5826
                                        }
5827
                                        //type no error shouldn't be treated
5828
                                        $next = 1;
5829
5830
                                        break;
5831
                                    }
5832
                                    if ($debug > 0) {
5833
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5834
                                    }
5835
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5836
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5837
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5838
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5839
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5840
5841
                                    if (false == $overlap) {
5842
                                        //all good, no overlap
5843
                                        $next = 1;
5844
5845
                                        break;
5846
                                    } else {
5847
                                        if ($debug > 0) {
5848
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5849
                                        }
5850
                                        $organs_at_risk_hit++;
5851
                                        //show the feedback
5852
                                        $next = 0;
5853
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5854
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5855
5856
                                        $destination_items = explode('@@', $answerDestination);
5857
                                        $try_hotspot = $destination_items[1];
5858
                                        $lp_hotspot = $destination_items[2];
5859
                                        $select_question_hotspot = $destination_items[3];
5860
                                        $url_hotspot = $destination_items[4];
5861
                                    }
5862
                                }
5863
                            }
5864
5865
                            break;
5866
                        case HOT_SPOT_ORDER:
5867
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5868
                                $feedback_type,
5869
                                $answerId,
5870
                                $answer,
5871
                                $studentChoice,
5872
                                $answerComment
5873
                            );*/
5874
5875
                            break;
5876
                        case DRAGGABLE:
5877
                        case MATCHING_DRAGGABLE:
5878
                        case MATCHING:
5879
                        case MATCHING_COMBINATION:
5880
                        case MATCHING_DRAGGABLE_COMBINATION:
5881
                            echo '<tr>';
5882
                            echo Display::tag('td', $answerMatching[$answerId]);
5883
                            echo Display::tag(
5884
                                'td',
5885
                                "$user_answer / ".Display::tag(
5886
                                    'strong',
5887
                                    $answerMatching[$answerCorrect],
5888
                                    ['style' => 'color: #008000; font-weight: bold;']
5889
                                )
5890
                            );
5891
                            echo '</tr>';
5892
5893
                            break;
5894
                        case ANNOTATION:
5895
                            ExerciseShowFunctions::displayAnnotationAnswer(
5896
                                $feedback_type,
5897
                                $exeId,
5898
                                $questionId,
5899
                                $questionScore,
5900
                                $results_disabled
5901
                            );
5902
5903
                            break;
5904
                    }
5905
                }
5906
            }
5907
        } // end for that loops over all answers of the current question
5908
5909
        if ($debug) {
5910
            error_log('-- End answer loop --');
5911
        }
5912
5913
        $final_answer = true;
5914
5915
        foreach ($real_answers as $my_answer) {
5916
            if (!$my_answer) {
5917
                $final_answer = false;
5918
            }
5919
        }
5920
5921
        if (FILL_IN_BLANKS_COMBINATION === $answerType) {
5922
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5923
                $answerType,
5924
                $listCorrectAnswers,
5925
                $exeId,
5926
                $questionId,
5927
                $questionWeighting
5928
            );
5929
        }
5930
5931
        if (HOT_SPOT_COMBINATION === $answerType) {
5932
            $listCoords = $exerciseResultCoordinates[$questionId] ?? [];
5933
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5934
                $answerType,
5935
                $listCoords,
5936
                $exeId,
5937
                $questionId,
5938
                $questionWeighting,
5939
                (array) ($choice ?? []),
5940
                $nbrAnswers
5941
            );
5942
        }
5943
5944
        if (in_array($answerType, [MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION], true)) {
5945
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5946
                $answerType,
5947
                $matchingCorrectAnswers[$questionId] ?? [],
5948
                $exeId,
5949
                $questionId,
5950
                $questionWeighting
5951
            );
5952
        }
5953
5954
        //we add the total score after dealing with the answers
5955
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
5956
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
5957
        ) {
5958
            if ($final_answer) {
5959
                //getting only the first score where we save the weight of all the question
5960
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5961
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
5962
                    $answerWeighting = $firstAnswer['ponderation'];
5963
                }
5964
                $questionScore += $answerWeighting;
5965
            }
5966
        }
5967
5968
        $extra_data = [
5969
            'final_overlap' => $final_overlap,
5970
            'final_missing' => $final_missing,
5971
            'final_excess' => $final_excess,
5972
            'overlap_color' => $overlap_color,
5973
            'missing_color' => $missing_color,
5974
            'excess_color' => $excess_color,
5975
            'threadhold1' => $threadhold1,
5976
            'threadhold2' => $threadhold2,
5977
            'threadhold3' => $threadhold3,
5978
        ];
5979
5980
        if ('exercise_result' === $from) {
5981
            // if answer is hotspot. To the difference of exercise_show.php,
5982
            //  we use the results from the session (from_db=0)
5983
            // TODO Change this, because it is wrong to show the user
5984
            //  some results that haven't been stored in the database yet
5985
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_DELINEATION, HOT_SPOT_COMBINATION])) {
5986
                if ($debug) {
5987
                    error_log('$from AND this is a hotspot kind of question ');
5988
                }
5989
                if (HOT_SPOT_DELINEATION === $answerType) {
5990
                    if ($showHotSpotDelineationTable) {
5991
                        if (!is_numeric($final_overlap)) {
5992
                            $final_overlap = 0;
5993
                        }
5994
                        if (!is_numeric($final_missing)) {
5995
                            $final_missing = 0;
5996
                        }
5997
                        if (!is_numeric($final_excess)) {
5998
                            $final_excess = 0;
5999
                        }
6000
6001
                        if ($final_overlap > 100) {
6002
                            $final_overlap = 100;
6003
                        }
6004
6005
                        $overlap = 0;
6006
                        if ($final_overlap > 0) {
6007
                            $overlap = (int) $final_overlap;
6008
                        }
6009
6010
                        $excess = 0;
6011
                        if ($final_excess > 0) {
6012
                            $excess = (int) $final_excess;
6013
                        }
6014
6015
                        $missing = 0;
6016
                        if ($final_missing > 0) {
6017
                            $missing = (int) $final_missing;
6018
                        }
6019
6020
                        $table_resume = '<table class="table table-hover table-striped data_table">
6021
                                <tr class="row_odd" >
6022
                                    <td></td>
6023
                                    <td ><b>'.get_lang('Requirements').'</b></td>
6024
                                    <td><b>'.get_lang('Your answer').'</b></td>
6025
                                </tr>
6026
                                <tr class="row_even">
6027
                                    <td><b>'.get_lang('Overlapping area').'</b></td>
6028
                                    <td>'.get_lang('Minimum').' '.$threadhold1.'</td>
6029
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-error').'">'
6030
                                    .$overlap.'</td>
6031
                                </tr>
6032
                                <tr>
6033
                                    <td><b>'.get_lang('Excessive area').'</b></td>
6034
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
6035
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-error').'">'
6036
                                    .$excess.'</td>
6037
                                </tr>
6038
                                <tr class="row_even">
6039
                                    <td><b>'.get_lang('Missing area').'</b></td>
6040
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
6041
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-error').'">'
6042
                                    .$missing.'</td>
6043
                                </tr>
6044
                            </table>';
6045
                        if (0 == $next) {
6046
                        } else {
6047
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
6048
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
6049
                        }
6050
6051
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
6052
                                    <p style="text-align:center">';
6053
                        $message .= '<p>'.get_lang('Your delineation :').'</p>';
6054
                        $message .= $table_resume;
6055
                        $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
6056
                        if ($organs_at_risk_hit > 0) {
6057
                            $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
6058
                        }
6059
                        $message .= '<p>'.$comment.'</p>';
6060
                        echo $message;
6061
6062
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][0] = $message;
6063
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
6064
                            $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
6065
                        }
6066
                    } else {
6067
                        echo $hotspot_delineation_result[0] ?? '';
6068
                    }
6069
6070
                    // Save the score attempts
6071
                    if (1) {
6072
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
6073
                        $final_answer = $hotspot_delineation_result[1] ?? '';
6074
                        if (0 == $final_answer) {
6075
                            $questionScore = 0;
6076
                        }
6077
                        // we always insert the answer_id 1 = delineation
6078
                        Event::saveQuestionAttempt($this, $questionScore, 1, $quesId, $exeId, 0);
6079
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
6080
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? 1 === (int) $hotspot_delineation_result[1] ? 1 : 0 : 0;
6081
                        Event::saveExerciseAttemptHotspot(
6082
                            $this,
6083
                            $exeId,
6084
                            $quesId,
6085
                            1,
6086
                            $hotspotValue,
6087
                            $exerciseResultCoordinates[$quesId] ?? '',
6088
                            false,
6089
                            0,
6090
                            $learnpath_id,
6091
                            $learnpath_item_id
6092
                        );
6093
                    } else {
6094
                        if (0 == $final_answer) {
6095
                            $questionScore = 0;
6096
                            $answer = 0;
6097
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
6098
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6099
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6100
                                    Event::saveExerciseAttemptHotspot(
6101
                                        $this,
6102
                                        $exeId,
6103
                                        $quesId,
6104
                                        $idx,
6105
                                        0,
6106
                                        $val,
6107
                                        false,
6108
                                        0,
6109
                                        $learnpath_id,
6110
                                        $learnpath_item_id
6111
                                    );
6112
                                }
6113
                            }
6114
                        } else {
6115
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
6116
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6117
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6118
                                    $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6119
                                    Event::saveExerciseAttemptHotspot(
6120
                                        $this,
6121
                                        $exeId,
6122
                                        $quesId,
6123
                                        $idx,
6124
                                        $hotspotValue,
6125
                                        $val,
6126
                                        false,
6127
                                        0,
6128
                                        $learnpath_id,
6129
                                        $learnpath_item_id
6130
                                    );
6131
                                }
6132
                            }
6133
                        }
6134
                    }
6135
                }
6136
            }
6137
6138
            $relPath = api_get_path(WEB_CODE_PATH);
6139
6140
            if ($save_results
6141
                && in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION], true)
6142
                && !empty($exerciseResultCoordinates[$questionId])
6143
            ) {
6144
                Database::delete(
6145
                    Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6146
                    [
6147
                        'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6148
                            $exeId, $questionId, api_get_course_int_id(),
6149
                        ],
6150
                    ]
6151
                );
6152
6153
                foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6154
                    $hotspotValue = !empty($choice[$idx]) ? 1 : 0;
6155
                    Event::saveExerciseAttemptHotspot(
6156
                        $this,
6157
                        $exeId,
6158
                        $quesId,
6159
                        $idx,
6160
                        $hotspotValue,
6161
                        $val,
6162
                        false,
6163
                        $this->id,
6164
                        $learnpath_id,
6165
                        $learnpath_item_id
6166
                    );
6167
                }
6168
                $hotspotsSavedEarly = true;
6169
            }
6170
6171
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_COMBINATION], true)) {
6172
                // We made an extra table for the answers
6173
                if ($show_result) {
6174
                    echo '</table></td></tr>';
6175
                    echo '
6176
                        <tr>
6177
                            <td colspan="2">
6178
                                <p><em>'.get_lang('Image zones')."</em></p>
6179
                                <div id=\"hotspot-solution-$questionId\"></div>
6180
                                <script>
6181
                                    $(function() {
6182
                                        new HotspotQuestion({
6183
                                            questionId: $questionId,
6184
                                            exerciseId: {$this->getId()},
6185
                                            exeId: $exeId,
6186
                                            selector: '#hotspot-solution-$questionId',
6187
                                            for: 'solution',
6188
                                            relPath: '$relPath'
6189
                                        });
6190
                                    });
6191
                                </script>
6192
                            </td>
6193
                        </tr>
6194
                    ";
6195
                }
6196
            } elseif (ANNOTATION == $answerType) {
6197
                if ($show_result) {
6198
                    echo '
6199
                        <p><em>'.get_lang('Annotation').'</em></p>
6200
                        <div id="annotation-canvas-'.$questionId.'"></div>
6201
                        <script>
6202
                            AnnotationQuestion({
6203
                                questionId: parseInt('.$questionId.'),
6204
                                exerciseId: parseInt('.$exeId.'),
6205
                                relPath: \''.$relPath.'\',
6206
                                courseId: parseInt('.$course_id.')
6207
                            });
6208
                        </script>
6209
                    ';
6210
                }
6211
            }
6212
6213
            if ($show_result && ANNOTATION != $answerType) {
6214
                echo '</table>';
6215
            }
6216
        }
6217
        unset($objAnswerTmp);
6218
6219
        $totalWeighting += $questionWeighting;
6220
        // Store results directly in the database
6221
        // For all in one page exercises, the results will be
6222
        // stored by exercise_results.php (using the session)
6223
        if (in_array(
6224
            $objQuestionTmp->type,
6225
            [PAGE_BREAK, MEDIA_QUESTION],
6226
            true
6227
        )) {
6228
            $save_results = false;
6229
        }
6230
        if ($save_results) {
6231
            if ($debug) {
6232
                error_log("Save question results $save_results");
6233
                error_log("Question score: $questionScore");
6234
                error_log('choice: ');
6235
                error_log(print_r($choice, 1));
6236
            }
6237
6238
            if (empty($choice)) {
6239
                $choice = 0;
6240
            }
6241
            // with certainty degree
6242
            if (empty($choiceDegreeCertainty)) {
6243
                $choiceDegreeCertainty = 0;
6244
            }
6245
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
6246
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType ||
6247
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
6248
            ) {
6249
                if (0 != $choice) {
6250
                    $reply = array_keys($choice);
6251
                    $countReply = count($reply);
6252
                    for ($i = 0; $i < $countReply; $i++) {
6253
                        $chosenAnswer = $reply[$i];
6254
                        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
6255
                            if (0 != $choiceDegreeCertainty) {
6256
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
6257
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
6258
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
6259
                                Event::saveQuestionAttempt(
6260
                                    $this,
6261
                                    $questionScore,
6262
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
6263
                                    $quesId,
6264
                                    $exeId,
6265
                                    $i,
6266
                                    $this->getId(),
6267
                                    $updateResults,
6268
                                    $questionDuration
6269
                                );
6270
                            }
6271
                        } else {
6272
                            Event::saveQuestionAttempt(
6273
                                $this,
6274
                                $questionScore,
6275
                                $chosenAnswer.':'.$choice[$chosenAnswer],
6276
                                $quesId,
6277
                                $exeId,
6278
                                $i,
6279
                                $this->getId(),
6280
                                $updateResults,
6281
                                $questionDuration
6282
                            );
6283
                        }
6284
                        if ($debug) {
6285
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
6286
                        }
6287
                    }
6288
                } else {
6289
                    Event::saveQuestionAttempt(
6290
                        $this,
6291
                        $questionScore,
6292
                        0,
6293
                        $quesId,
6294
                        $exeId,
6295
                        0,
6296
                        $this->getId(),
6297
                        false,
6298
                        $questionDuration
6299
                    );
6300
                }
6301
            } elseif (in_array($answerType, [
6302
                MULTIPLE_ANSWER,
6303
                GLOBAL_MULTIPLE_ANSWER,
6304
                MULTIPLE_ANSWER_DROPDOWN,
6305
                MULTIPLE_ANSWER_DROPDOWN_COMBINATION
6306
            ], true)) {
6307
                if (0 != $choice) {
6308
                    $reply = in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION], true)
6309
                        ? array_values((array) $choice)
6310
                        : array_keys((array) $choice);
6311
                    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...
6312
                        $ans = $reply[$i];
6313
                        Event::saveQuestionAttempt(
6314
                            $this,
6315
                            $questionScore,
6316
                            $ans,
6317
                            $quesId,
6318
                            $exeId,
6319
                            $i,
6320
                            $this->id,
6321
                            false,
6322
                            $questionDuration
6323
                        );
6324
                    }
6325
                } else {
6326
                    Event::saveQuestionAttempt(
6327
                        $this,
6328
                        $questionScore,
6329
                        0,
6330
                        $quesId,
6331
                        $exeId,
6332
                        0,
6333
                        $this->id,
6334
                        false,
6335
                        $questionDuration
6336
                    );
6337
                }
6338
            } elseif (MULTIPLE_ANSWER_COMBINATION == $answerType) {
6339
                if (0 != $choice) {
6340
                    $reply = array_keys($choice);
6341
                    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...
6342
                        $ans = $reply[$i];
6343
                        Event::saveQuestionAttempt(
6344
                            $this,
6345
                            $questionScore,
6346
                            $ans,
6347
                            $quesId,
6348
                            $exeId,
6349
                            $i,
6350
                            $this->id,
6351
                            false,
6352
                            $questionDuration
6353
                        );
6354
                    }
6355
                } else {
6356
                    Event::saveQuestionAttempt(
6357
                        $this,
6358
                        $questionScore,
6359
                        0,
6360
                        $quesId,
6361
                        $exeId,
6362
                        0,
6363
                        $this->id,
6364
                        false,
6365
                        $questionDuration
6366
                    );
6367
                }
6368
            } elseif (in_array($answerType, [
6369
                MATCHING,
6370
                DRAGGABLE,
6371
                MATCHING_DRAGGABLE,
6372
                MATCHING_COMBINATION,
6373
                MATCHING_DRAGGABLE_COMBINATION
6374
            ], true)) {
6375
                if (isset($matching)) {
6376
                    foreach ($matching as $j => $val) {
6377
                        Event::saveQuestionAttempt(
6378
                            $this,
6379
                            $questionScore,
6380
                            $val,
6381
                            $quesId,
6382
                            $exeId,
6383
                            $j,
6384
                            $this->id,
6385
                            false,
6386
                            $questionDuration
6387
                        );
6388
                    }
6389
                }
6390
            } elseif (FREE_ANSWER == $answerType) {
6391
                $answer = $choice;
6392
                Event::saveQuestionAttempt(
6393
                    $this,
6394
                    $questionScore,
6395
                    $answer,
6396
                    $quesId,
6397
                    $exeId,
6398
                    0,
6399
                    $this->id,
6400
                    false,
6401
                    $questionDuration
6402
                );
6403
            } elseif (ORAL_EXPRESSION == $answerType) {
6404
                $answer = $choice;
6405
                /** @var OralExpression $objQuestionTmp */
6406
                $questionAttemptId = Event::saveQuestionAttempt(
6407
                    $this,
6408
                    $questionScore,
6409
                    $answer,
6410
                    $quesId,
6411
                    $exeId,
6412
                    0,
6413
                    $this->id,
6414
                    false,
6415
                    $questionDuration
6416
                );
6417
6418
                if (false !== $questionAttemptId) {
6419
                    OralExpression::saveAssetInQuestionAttempt($questionAttemptId);
6420
                }
6421
            } elseif (
6422
            in_array(
6423
                $answerType,
6424
                [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6425
            )
6426
            ) {
6427
                $answer = $choice;
6428
                Event::saveQuestionAttempt(
6429
                    $this,
6430
                    $questionScore,
6431
                    $answer,
6432
                    $quesId,
6433
                    $exeId,
6434
                    0,
6435
                    $this->id,
6436
                    false,
6437
                    $questionDuration
6438
                );
6439
            } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, ANNOTATION], true)) {
6440
                $answer = [];
6441
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6442
                    if ($debug) {
6443
                        error_log('Checking result coordinates');
6444
                    }
6445
                    Database::delete(
6446
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6447
                        [
6448
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6449
                                $exeId,
6450
                                $questionId,
6451
                                api_get_course_int_id(),
6452
                            ],
6453
                        ]
6454
                    );
6455
6456
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6457
                        $answer[] = $val;
6458
                        $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6459
                        if ($debug) {
6460
                            error_log('Hotspot value: '.$hotspotValue);
6461
                        }
6462
                        Event::saveExerciseAttemptHotspot(
6463
                            $this,
6464
                            $exeId,
6465
                            $quesId,
6466
                            $idx,
6467
                            $hotspotValue,
6468
                            $val,
6469
                            false,
6470
                            $this->id,
6471
                            $learnpath_id,
6472
                            $learnpath_item_id
6473
                        );
6474
                    }
6475
                } else {
6476
                    if ($debug) {
6477
                        error_log('Empty: exerciseResultCoordinates');
6478
                    }
6479
                }
6480
                Event::saveQuestionAttempt(
6481
                    $this,
6482
                    $questionScore,
6483
                    implode('|', $answer),
6484
                    $quesId,
6485
                    $exeId,
6486
                    0,
6487
                    $this->id,
6488
                    false,
6489
                    $questionDuration
6490
                );
6491
            } else {
6492
                Event::saveQuestionAttempt(
6493
                    $this,
6494
                    $questionScore,
6495
                    $answer,
6496
                    $quesId,
6497
                    $exeId,
6498
                    0,
6499
                    $this->id,
6500
                    false,
6501
                    $questionDuration
6502
                );
6503
            }
6504
        }
6505
6506
        if (0 == $propagate_neg && $questionScore < 0) {
6507
            $questionScore = 0;
6508
        }
6509
6510
        if ($save_results) {
6511
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6512
            $sql = "UPDATE $statsTable SET
6513
                        score = score + ".(float) $questionScore."
6514
                    WHERE exe_id = $exeId";
6515
            Database::query($sql);
6516
        }
6517
6518
        return [
6519
            'score' => $questionScore,
6520
            'weight' => $questionWeighting,
6521
            'extra' => $extra_data,
6522
            'open_question' => $arrques,
6523
            'open_answer' => $arrans,
6524
            'answer_type' => $answerType,
6525
            'generated_oral_file' => $generatedFilesHtml,
6526
            'user_answered' => $userAnsweredQuestion,
6527
            'correct_answer_id' => $correctAnswerId,
6528
            'answer_destination' => $answerDestination,
6529
        ];
6530
    }
6531
6532
    /**
6533
     * Sends a notification when a user ends an examn.
6534
     *
6535
     * @param string $type                  'start' or 'end' of an exercise
6536
     * @param array  $question_list_answers
6537
     * @param string $origin
6538
     * @param int    $exe_id
6539
     * @param float  $score
6540
     * @param float  $weight
6541
     *
6542
     * @return bool
6543
     */
6544
    public function send_mail_notification_for_exam(
6545
        $type,
6546
        $question_list_answers,
6547
        $origin,
6548
        $exe_id,
6549
        $score = null,
6550
        $weight = null
6551
    ) {
6552
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6553
6554
        if ((empty($setting) || !is_array($setting)) && empty($this->getNotifications())) {
6555
            return false;
6556
        }
6557
6558
        $settingFromExercise = $this->getNotifications();
6559
        if (!empty($settingFromExercise)) {
6560
            $setting = $settingFromExercise;
6561
        }
6562
6563
        // Email configuration settings
6564
        $courseCode = api_get_course_id();
6565
        $courseInfo = api_get_course_info($courseCode);
6566
6567
        if (empty($courseInfo)) {
6568
            return false;
6569
        }
6570
6571
        $sessionId = api_get_session_id();
6572
6573
        $sessionData = '';
6574
        if (!empty($sessionId)) {
6575
            $sessionInfo = api_get_session_info($sessionId);
6576
            if (!empty($sessionInfo)) {
6577
                $sessionData = '<tr>'
6578
                    .'<td>'.get_lang('Session name').'</td>'
6579
                    .'<td>'.$sessionInfo['name'].'</td>'
6580
                    .'</tr>';
6581
            }
6582
        }
6583
6584
        $sendStart = false;
6585
        $sendEnd = false;
6586
        $sendEndOpenQuestion = false;
6587
        $sendEndOralQuestion = false;
6588
6589
        foreach ($setting as $option) {
6590
            switch ($option) {
6591
                case 0:
6592
                    return false;
6593
6594
                    break;
6595
                case 1: // End
6596
                    if ('end' == $type) {
6597
                        $sendEnd = true;
6598
                    }
6599
6600
                    break;
6601
                case 2: // start
6602
                    if ('start' == $type) {
6603
                        $sendStart = true;
6604
                    }
6605
6606
                    break;
6607
                case 3: // end + open
6608
                    if ('end' == $type) {
6609
                        $sendEndOpenQuestion = true;
6610
                    }
6611
6612
                    break;
6613
                case 4: // end + oral
6614
                    if ('end' == $type) {
6615
                        $sendEndOralQuestion = true;
6616
                    }
6617
6618
                    break;
6619
            }
6620
        }
6621
6622
        $user_info = api_get_user_info(api_get_user_id());
6623
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6624
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6625
6626
        if (!empty($sessionId)) {
6627
            $addGeneralCoach = true;
6628
            $setting = ('true' === api_get_setting('exercise.block_quiz_mail_notification_general_coach'));
6629
            if (true === $setting) {
6630
                $addGeneralCoach = false;
6631
            }
6632
            $teachers = CourseManager::get_coach_list_from_course_code(
6633
                $courseCode,
6634
                $sessionId,
6635
                $addGeneralCoach
6636
            );
6637
        } else {
6638
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6639
        }
6640
6641
        if ($sendEndOpenQuestion) {
6642
            $this->sendNotificationForOpenQuestions(
6643
                $question_list_answers,
6644
                $origin,
6645
                $user_info,
6646
                $url,
6647
                $teachers
6648
            );
6649
        }
6650
6651
        if ($sendEndOralQuestion) {
6652
            $this->sendNotificationForOralQuestions(
6653
                $question_list_answers,
6654
                $origin,
6655
                $exe_id,
6656
                $user_info,
6657
                $url,
6658
                $teachers
6659
            );
6660
        }
6661
6662
        if (!$sendEnd && !$sendStart) {
6663
            return false;
6664
        }
6665
6666
        $scoreLabel = '';
6667
        if ($sendEnd &&
6668
            ('true' === api_get_setting('exercise.send_score_in_exam_notification_mail_to_manager'))
6669
        ) {
6670
            $notificationPercentage = ('true' === api_get_setting('mail.send_notification_score_in_percentage'));
6671
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6672
            $scoreLabel = '<tr>
6673
                            <td>'.get_lang('Score')."</td>
6674
                            <td>&nbsp;$scoreLabel</td>
6675
                        </tr>";
6676
        }
6677
6678
        if ($sendEnd) {
6679
            $msg = get_lang('A learner attempted an exercise').'<br /><br />';
6680
        } else {
6681
            $msg = get_lang('Student just started an exercise').'<br /><br />';
6682
        }
6683
6684
        $msg .= get_lang('Attempt details').' : <br /><br />
6685
                    <table>
6686
                        <tr>
6687
                            <td>'.get_lang('Course name').'</td>
6688
                            <td>#course#</td>
6689
                        </tr>
6690
                        '.$sessionData.'
6691
                        <tr>
6692
                            <td>'.get_lang('Test').'</td>
6693
                            <td>&nbsp;#exercise#</td>
6694
                        </tr>
6695
                        <tr>
6696
                            <td>'.get_lang('Learner name').'</td>
6697
                            <td>&nbsp;#student_complete_name#</td>
6698
                        </tr>
6699
                        <tr>
6700
                            <td>'.get_lang('Learner e-mail').'</td>
6701
                            <td>&nbsp;#email#</td>
6702
                        </tr>
6703
                        '.$scoreLabel.'
6704
                    </table>';
6705
6706
        $variables = [
6707
            '#email#' => $user_info['email'],
6708
            '#exercise#' => $this->exercise,
6709
            '#student_complete_name#' => $user_info['complete_name'],
6710
            '#course#' => Display::url(
6711
                $courseInfo['title'],
6712
                $courseInfo['course_public_url'].'?sid='.$sessionId
6713
            ),
6714
        ];
6715
6716
        if ($sendEnd) {
6717
            $msg .= '<br /><a href="#url#">'.get_lang(
6718
                    'Click this link to check the answer and/or give feedback'
6719
                ).'</a>';
6720
            $variables['#url#'] = $url;
6721
        }
6722
6723
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6724
6725
        if ($sendEnd) {
6726
            $subject = get_lang('A learner attempted an exercise');
6727
        } else {
6728
            $subject = get_lang('Student just started an exercise');
6729
        }
6730
6731
        if (!empty($teachers)) {
6732
            foreach ($teachers as $user_id => $teacher_data) {
6733
                MessageManager::send_message_simple(
6734
                    $user_id,
6735
                    $subject,
6736
                    $content
6737
                );
6738
            }
6739
        }
6740
    }
6741
6742
    /**
6743
     * @param array $user_data         result of api_get_user_info()
6744
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6745
     * @param bool  $saveUserResult
6746
     * @param bool  $allowSignature
6747
     * @param bool  $allowExportPdf
6748
     *
6749
     * @return string
6750
     */
6751
    public function showExerciseResultHeader(
6752
        $user_data,
6753
        $trackExerciseInfo,
6754
        $saveUserResult,
6755
        $allowSignature = false,
6756
        $allowExportPdf = false
6757
    ) {
6758
        if ('true' === api_get_setting('exercise.hide_user_info_in_quiz_result')) {
6759
            return '';
6760
        }
6761
6762
        $start_date = null;
6763
        if (isset($trackExerciseInfo['start_date'])) {
6764
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6765
        }
6766
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6767
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6768
6769
        if (!empty($user_data)) {
6770
            $userFullName = $user_data['complete_name'];
6771
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6772
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('Go to learner details').'">'.
6773
                    $user_data['complete_name'].'</a>';
6774
            }
6775
6776
            $data = [
6777
                'name_url' => $userFullName,
6778
                'complete_name' => $user_data['complete_name'],
6779
                'username' => $user_data['username'],
6780
                'avatar' => $user_data['avatar_medium'],
6781
                'url' => $user_data['profile_url'],
6782
            ];
6783
6784
            if (!empty($user_data['official_code'])) {
6785
                $data['code'] = $user_data['official_code'];
6786
            }
6787
        }
6788
        // Description can be very long and is generally meant to explain
6789
        //   rules *before* the exam. Leaving here to make display easier if
6790
        //   necessary
6791
        /*
6792
        if (!empty($this->description)) {
6793
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6794
        }
6795
        */
6796
6797
        $data['start_date'] = $start_date;
6798
        $data['duration'] = $duration;
6799
        $data['ip'] = $ip;
6800
6801
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
6802
            $data['title'] = $this->get_formated_title().get_lang('Result');
6803
        } else {
6804
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6805
        }
6806
6807
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6808
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6809
6810
        $data['number_of_answers'] = $questionsCount;
6811
        $data['number_of_answers_saved'] = $savedAnswersCount;
6812
        $exeId = $trackExerciseInfo['exe_id'];
6813
6814
        if ('true' === api_get_setting('exercise.quiz_confirm_saved_answers')) {
6815
            $em = Database::getManager();
6816
6817
            if ($saveUserResult) {
6818
                $trackConfirmation = new TrackEExerciseConfirmation();
6819
                $trackConfirmation
6820
                    ->setUser(api_get_user_entity($trackExerciseInfo['exe_user_id']))
6821
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6822
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6823
                    ->setQuestionsCount($questionsCount)
6824
                    ->setSavedAnswersCount($savedAnswersCount)
6825
                    ->setCourseId($trackExerciseInfo['c_id'])
6826
                    ->setSessionId($trackExerciseInfo['session_id'])
6827
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6828
6829
                $em->persist($trackConfirmation);
6830
                $em->flush();
6831
            } else {
6832
                $trackConfirmation = $em
6833
                    ->getRepository(TrackEExerciseConfirmation::class)
6834
                    ->findOneBy(
6835
                        [
6836
                            'attemptId' => $trackExerciseInfo['exe_id'],
6837
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6838
                            'courseId' => $trackExerciseInfo['c_id'],
6839
                            'sessionId' => $trackExerciseInfo['session_id'],
6840
                        ]
6841
                    );
6842
            }
6843
6844
            $data['track_confirmation'] = $trackConfirmation;
6845
        }
6846
6847
        $signature = '';
6848
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6849
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6850
        }
6851
        $tpl = new Template(null, false, false, false, false, false, false);
6852
        $tpl->assign('data', $data);
6853
        $tpl->assign('allow_signature', $allowSignature);
6854
        $tpl->assign('signature', $signature);
6855
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6856
        $tpl->assign(
6857
            'export_url',
6858
            api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq()
6859
        );
6860
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6861
6862
        return $tpl->fetch($layoutTemplate);
6863
    }
6864
6865
    /**
6866
     * Returns the exercise result.
6867
     *
6868
     * @param int        attempt id
6869
     *
6870
     * @return array
6871
     */
6872
    public function get_exercise_result($exe_id)
6873
    {
6874
        $result = [];
6875
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6876
6877
        if (!empty($track_exercise_info)) {
6878
            $totalScore = 0;
6879
            $objExercise = new self();
6880
            $objExercise->read($track_exercise_info['exe_exo_id']);
6881
            if (!empty($track_exercise_info['data_tracking'])) {
6882
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6883
            }
6884
            foreach ($question_list as $questionId) {
6885
                $question_result = $objExercise->manage_answer(
6886
                    $exe_id,
6887
                    $questionId,
6888
                    '',
6889
                    'exercise_show',
6890
                    [],
6891
                    false,
6892
                    true,
6893
                    false,
6894
                    $objExercise->selectPropagateNeg()
6895
                );
6896
                $totalScore += $question_result['score'];
6897
            }
6898
6899
            if (0 == $objExercise->selectPropagateNeg() && $totalScore < 0) {
6900
                $totalScore = 0;
6901
            }
6902
            $result = [
6903
                'score' => $totalScore,
6904
                'weight' => $track_exercise_info['max_score'],
6905
            ];
6906
        }
6907
6908
        return $result;
6909
    }
6910
6911
    /**
6912
     * Checks if the exercise is visible due a lot of conditions
6913
     * visibility, time limits, student attempts
6914
     * Return associative array
6915
     * value : true if exercise visible
6916
     * message : HTML formatted message
6917
     * rawMessage : text message.
6918
     *
6919
     * @param int  $lpId
6920
     * @param int  $lpItemId
6921
     * @param int  $lpItemViewId
6922
     * @param bool $filterByAdmin
6923
     *
6924
     * @return array
6925
     */
6926
    public function is_visible(
6927
        $lpId = 0,
6928
        $lpItemId = 0,
6929
        $lpItemViewId = 0,
6930
        $filterByAdmin = true
6931
    ) {
6932
        // 1. By default the exercise is visible
6933
        $isVisible = true;
6934
        $message = null;
6935
6936
        // 1.1 Admins and teachers can access to the exercise
6937
        if ($filterByAdmin) {
6938
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6939
                return ['value' => true, 'message' => ''];
6940
            }
6941
        }
6942
6943
        // Deleted exercise.
6944
        if (-1 == $this->active) {
6945
            return [
6946
                'value' => false,
6947
                'message' => Display::return_message(
6948
                    get_lang('Test not found or not visible'),
6949
                    'warning',
6950
                    false
6951
                ),
6952
                'rawMessage' => get_lang('Test not found or not visible'),
6953
            ];
6954
        }
6955
6956
        $repo = Container::getQuizRepository();
6957
        $exercise = $repo->find($this->iId);
6958
6959
        if (null === $exercise) {
6960
            return [];
6961
        }
6962
6963
        $course = api_get_course_entity($this->course_id);
6964
        $link = $exercise->getFirstResourceLinkFromCourseSession($course);
6965
6966
        if ($link && $link->isDraft()) {
6967
            $this->active = 0;
6968
        }
6969
6970
        // 2. If the exercise is not active.
6971
        if (empty($lpId)) {
6972
            // 2.1 LP is OFF
6973
            if (0 == $this->active) {
6974
                return [
6975
                    'value' => false,
6976
                    'message' => Display::return_message(
6977
                        get_lang('Test not found or not visible'),
6978
                        'warning',
6979
                        false
6980
                    ),
6981
                    'rawMessage' => get_lang('Test not found or not visible'),
6982
                ];
6983
            }
6984
        } else {
6985
            $lp = Container::getLpRepository()->find($lpId);
6986
            // 2.1 LP is loaded
6987
            if ($lp && 0 == $this->active &&
6988
                !learnpath::is_lp_visible_for_student($lp, api_get_user_id(), $course)
6989
            ) {
6990
                return [
6991
                    'value' => false,
6992
                    'message' => Display::return_message(
6993
                        get_lang('Test not found or not visible'),
6994
                        'warning',
6995
                        false
6996
                    ),
6997
                    'rawMessage' => get_lang('Test not found or not visible'),
6998
                ];
6999
            }
7000
        }
7001
7002
        // 3. We check if the time limits are on
7003
        $limitTimeExists = false;
7004
        if (!empty($this->start_time) || !empty($this->end_time)) {
7005
            $limitTimeExists = true;
7006
        }
7007
7008
        if ($limitTimeExists) {
7009
            $timeNow = time();
7010
            $existsStartDate = false;
7011
            $nowIsAfterStartDate = true;
7012
            $existsEndDate = false;
7013
            $nowIsBeforeEndDate = true;
7014
7015
            if (!empty($this->start_time)) {
7016
                $existsStartDate = true;
7017
            }
7018
7019
            if (!empty($this->end_time)) {
7020
                $existsEndDate = true;
7021
            }
7022
7023
            // check if we are before-or-after end-or-start date
7024
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
7025
                $nowIsAfterStartDate = false;
7026
            }
7027
7028
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
7029
                $nowIsBeforeEndDate = false;
7030
            }
7031
7032
            // lets check all cases
7033
            if ($existsStartDate && !$existsEndDate) {
7034
                // exists start date and dont exists end date
7035
                if ($nowIsAfterStartDate) {
7036
                    // after start date, no end date
7037
                    $isVisible = true;
7038
                    $message = sprintf(
7039
                        get_lang('Exercise available since %s'),
7040
                        api_convert_and_format_date($this->start_time)
7041
                    );
7042
                } else {
7043
                    // before start date, no end date
7044
                    $isVisible = false;
7045
                    $message = sprintf(
7046
                        get_lang('Exercise available from %s'),
7047
                        api_convert_and_format_date($this->start_time)
7048
                    );
7049
                }
7050
            } elseif (!$existsStartDate && $existsEndDate) {
7051
                // doesnt exist start date, exists end date
7052
                if ($nowIsBeforeEndDate) {
7053
                    // before end date, no start date
7054
                    $isVisible = true;
7055
                    $message = sprintf(
7056
                        get_lang('Exercise available until %s'),
7057
                        api_convert_and_format_date($this->end_time)
7058
                    );
7059
                } else {
7060
                    // after end date, no start date
7061
                    $isVisible = false;
7062
                    $message = sprintf(
7063
                        get_lang('Exercise available until %s'),
7064
                        api_convert_and_format_date($this->end_time)
7065
                    );
7066
                }
7067
            } elseif ($existsStartDate && $existsEndDate) {
7068
                // exists start date and end date
7069
                if ($nowIsAfterStartDate) {
7070
                    if ($nowIsBeforeEndDate) {
7071
                        // after start date and before end date
7072
                        $isVisible = true;
7073
                        $message = sprintf(
7074
                            get_lang('Exercise was activated from %s to %s'),
7075
                            api_convert_and_format_date($this->start_time),
7076
                            api_convert_and_format_date($this->end_time)
7077
                        );
7078
                    } else {
7079
                        // after start date and after end date
7080
                        $isVisible = false;
7081
                        $message = sprintf(
7082
                            get_lang('Exercise was activated from %s to %s'),
7083
                            api_convert_and_format_date($this->start_time),
7084
                            api_convert_and_format_date($this->end_time)
7085
                        );
7086
                    }
7087
                } else {
7088
                    if ($nowIsBeforeEndDate) {
7089
                        // before start date and before end date
7090
                        $isVisible = false;
7091
                        $message = sprintf(
7092
                            get_lang('Exercise will be activated from %s to %s'),
7093
                            api_convert_and_format_date($this->start_time),
7094
                            api_convert_and_format_date($this->end_time)
7095
                        );
7096
                    }
7097
                    // case before start date and after end date is impossible
7098
                }
7099
            } elseif (!$existsStartDate && !$existsEndDate) {
7100
                // doesnt exist start date nor end date
7101
                $isVisible = true;
7102
                $message = '';
7103
            }
7104
        }
7105
7106
        // 4. We check if the student have attempts
7107
        if ($isVisible) {
7108
            $exerciseAttempts = $this->selectAttempts();
7109
7110
            if ($exerciseAttempts > 0) {
7111
                $attemptCount = Event::get_attempt_count(
7112
                    api_get_user_id(),
7113
                    $this->getId(),
7114
                    (int) $lpId,
7115
                    (int) $lpItemId,
7116
                    (int) $lpItemViewId
7117
                );
7118
7119
                if ($attemptCount >= $exerciseAttempts) {
7120
                    $message = sprintf(
7121
                        get_lang('You cannot take test <b>%s</b> because you have already reached the maximum of %s attempts.'),
7122
                        $this->name,
7123
                        $exerciseAttempts
7124
                    );
7125
                    $isVisible = false;
7126
                } else {
7127
                    // Check blocking exercise.
7128
                    $extraFieldValue = new ExtraFieldValue('exercise');
7129
                    $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
7130
                        $this->iId,
7131
                        'blocking_percentage'
7132
                    );
7133
                    if ($blockExercise && isset($blockExercise['value']) && !empty($blockExercise['value'])) {
7134
                        $blockPercentage = (int) $blockExercise['value'];
7135
                        $userAttempts = Event::getExerciseResultsByUser(
7136
                            api_get_user_id(),
7137
                            $this->iId,
7138
                            $this->course_id,
7139
                            $this->sessionId,
7140
                            $lpId,
7141
                            $lpItemId
7142
                        );
7143
7144
                        if (!empty($userAttempts)) {
7145
                            $currentAttempt = current($userAttempts);
7146
                            if ($currentAttempt['total_percentage'] <= $blockPercentage) {
7147
                                $message = sprintf(
7148
                                    get_lang('All attempts blocked because you did not reach the minimum score of %s % at one of your attempts.'),
7149
                                    $blockPercentage
7150
                                );
7151
                                $isVisible = false;
7152
                            }
7153
                        }
7154
                    }
7155
                }
7156
            }
7157
        }
7158
7159
        $rawMessage = '';
7160
        if (!empty($message)) {
7161
            $rawMessage = $message;
7162
            $message = Display::return_message($message, 'warning', false);
7163
        }
7164
7165
        return [
7166
            'value' => $isVisible,
7167
            'message' => $message,
7168
            'rawMessage' => $rawMessage,
7169
        ];
7170
    }
7171
7172
    /**
7173
     * @return bool
7174
     */
7175
    public function added_in_lp()
7176
    {
7177
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
7178
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
7179
                WHERE
7180
                    item_type = '".TOOL_QUIZ."' AND
7181
                    path = '{$this->getId()}'";
7182
        $result = Database::query($sql);
7183
        if (Database::num_rows($result) > 0) {
7184
            return true;
7185
        }
7186
7187
        return false;
7188
    }
7189
7190
    /**
7191
     * Returns an array with this form.
7192
     *
7193
     * @return array
7194
     *
7195
     * @example
7196
     * <code>
7197
     * array (size=3)
7198
     * 999 =>
7199
     * array (size=3)
7200
     * 0 => int 3422
7201
     * 1 => int 3423
7202
     * 2 => int 3424
7203
     * 100 =>
7204
     * array (size=2)
7205
     * 0 => int 3469
7206
     * 1 => int 3470
7207
     * 101 =>
7208
     * array (size=1)
7209
     * 0 => int 3482
7210
     * </code>
7211
     * The array inside the key 999 means the question list that belongs to the media id = 999,
7212
     * this case is special because 999 means "no media".
7213
     */
7214
    public function getMediaList()
7215
    {
7216
        return $this->mediaList;
7217
    }
7218
7219
    /**
7220
     * Is media question activated?
7221
     *
7222
     * @return bool
7223
     */
7224
    public function mediaIsActivated()
7225
    {
7226
        $mediaQuestions = $this->getMediaList();
7227
        $active = false;
7228
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
7229
            $media_count = count($mediaQuestions);
7230
            if ($media_count > 1) {
7231
                return true;
7232
            } elseif (1 == $media_count) {
7233
                if (isset($mediaQuestions[999])) {
7234
                    return false;
7235
                } else {
7236
                    return true;
7237
                }
7238
            }
7239
        }
7240
7241
        return $active;
7242
    }
7243
7244
    /**
7245
     * Gets question list from the exercise.
7246
     *
7247
     * @return array
7248
     */
7249
    public function getQuestionList()
7250
    {
7251
        return $this->questionList;
7252
    }
7253
7254
    /**
7255
     * Question list with medias compressed like this.
7256
     *
7257
     * @return array
7258
     *
7259
     * @example
7260
     *      <code>
7261
     *      array(
7262
     *      question_id_1,
7263
     *      question_id_2,
7264
     *      media_id, <- this media id contains question ids
7265
     *      question_id_3,
7266
     *      )
7267
     *      </code>
7268
     */
7269
    public function getQuestionListWithMediasCompressed()
7270
    {
7271
        return $this->questionList;
7272
    }
7273
7274
    /**
7275
     * Question list with medias uncompressed like this.
7276
     *
7277
     * @return array
7278
     *
7279
     * @example
7280
     *      <code>
7281
     *      array(
7282
     *      question_id,
7283
     *      question_id,
7284
     *      question_id, <- belongs to a media id
7285
     *      question_id, <- belongs to a media id
7286
     *      question_id,
7287
     *      )
7288
     *      </code>
7289
     */
7290
    public function getQuestionListWithMediasUncompressed()
7291
    {
7292
        return $this->questionListUncompressed;
7293
    }
7294
7295
    /**
7296
     * Sets the question list when the exercise->read() is executed.
7297
     *
7298
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
7299
     */
7300
    public function setQuestionList($adminView = false)
7301
    {
7302
        // Getting question list.
7303
        $questionList = $this->selectQuestionList(true, $adminView);
7304
        $this->setMediaList($questionList);
7305
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
7306
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
7307
            $questionList,
7308
            true
7309
        );
7310
    }
7311
7312
    /**
7313
     * @params array question list
7314
     * @params bool expand or not question list (true show all questions,
7315
     * false show media question id instead of the question ids)
7316
     */
7317
    public function transformQuestionListWithMedias(
7318
        $question_list,
7319
        $expand_media_questions = false
7320
    ) {
7321
        $new_question_list = [];
7322
        if (!empty($question_list)) {
7323
            $media_questions = $this->getMediaList();
7324
            $media_active = $this->mediaIsActivated($media_questions);
7325
7326
            if ($media_active) {
7327
                $counter = 1;
7328
                foreach ($question_list as $question_id) {
7329
                    $add_question = true;
7330
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7331
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7332
                            $add_question = false;
7333
                            if (!in_array($media_id, $new_question_list)) {
7334
                                $new_question_list[$counter] = $media_id;
7335
                                $counter++;
7336
                            }
7337
7338
                            break;
7339
                        }
7340
                    }
7341
                    if ($add_question) {
7342
                        $new_question_list[$counter] = $question_id;
7343
                        $counter++;
7344
                    }
7345
                }
7346
                if ($expand_media_questions) {
7347
                    $media_key_list = array_keys($media_questions);
7348
                    foreach ($new_question_list as &$question_id) {
7349
                        if (in_array($question_id, $media_key_list)) {
7350
                            $question_id = $media_questions[$question_id];
7351
                        }
7352
                    }
7353
                    $new_question_list = array_flatten($new_question_list);
7354
                }
7355
            } else {
7356
                $new_question_list = $question_list;
7357
            }
7358
        }
7359
7360
        return $new_question_list;
7361
    }
7362
7363
    /**
7364
     * Get question list depend on the random settings.
7365
     *
7366
     * @return array
7367
     */
7368
    public function get_validated_question_list()
7369
    {
7370
        $isRandomByCategory = $this->isRandomByCat();
7371
        if (0 == $isRandomByCategory) {
7372
            if ($this->isRandom()) {
7373
                return $this->getRandomList();
7374
            }
7375
7376
            return $this->selectQuestionList();
7377
        }
7378
7379
        if ($this->isRandom()) {
7380
            // USE question categories
7381
            // get questions by category for this exercise
7382
            // we have to choice $objExercise->random question in each array values of $tabCategoryQuestions
7383
            // key of $tabCategoryQuestions are the categopy id (0 for not in a category)
7384
            // value is the array of question id of this category
7385
            $questionList = [];
7386
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
7387
            $isRandomByCategory = $this->getRandomByCategory();
7388
            // We sort categories based on the term between [] in the head
7389
            // of the category's description
7390
            /* examples of categories :
7391
             * [biologie] Maitriser les mecanismes de base de la genetique
7392
             * [biologie] Relier les moyens de depenses et les agents infectieux
7393
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
7394
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
7395
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
7396
             * [chimie] Connaître les charges des particules
7397
             * We want that in the order of the groups defined by the term
7398
             * between brackets at the beginning of the category title
7399
            */
7400
            // If test option is Grouped By Categories
7401
            if (2 == $isRandomByCategory) {
7402
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
7403
            }
7404
            foreach ($categoryQuestions as $question) {
7405
                $number_of_random_question = $this->random;
7406
                if (-1 == $this->random) {
7407
                    $number_of_random_question = count($this->questionList);
7408
                }
7409
                $questionList = array_merge(
7410
                    $questionList,
7411
                    TestCategory::getNElementsFromArray(
7412
                        $question,
7413
                        $number_of_random_question
7414
                    )
7415
                );
7416
            }
7417
            // shuffle the question list if test is not grouped by categories
7418
            if (1 == $isRandomByCategory) {
7419
                shuffle($questionList); // or not
7420
            }
7421
7422
            return $questionList;
7423
        }
7424
7425
        // Problem, random by category has been selected and
7426
        // we have no $this->isRandom number of question selected
7427
        // Should not happened
7428
7429
        return [];
7430
    }
7431
7432
    public function get_question_list($expand_media_questions = false)
7433
    {
7434
        $question_list = $this->get_validated_question_list();
7435
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
7436
7437
        return $question_list;
7438
    }
7439
7440
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
7441
    {
7442
        $new_question_list = [];
7443
        if (!empty($question_list)) {
7444
            $media_questions = $this->getMediaList();
7445
            $media_active = $this->mediaIsActivated($media_questions);
7446
7447
            if ($media_active) {
7448
                $counter = 1;
7449
                foreach ($question_list as $question_id) {
7450
                    $add_question = true;
7451
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7452
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7453
                            $add_question = false;
7454
                            if (!in_array($media_id, $new_question_list)) {
7455
                                $new_question_list[$counter] = $media_id;
7456
                                $counter++;
7457
                            }
7458
7459
                            break;
7460
                        }
7461
                    }
7462
                    if ($add_question) {
7463
                        $new_question_list[$counter] = $question_id;
7464
                        $counter++;
7465
                    }
7466
                }
7467
                if ($expand_media_questions) {
7468
                    $media_key_list = array_keys($media_questions);
7469
                    foreach ($new_question_list as &$question_id) {
7470
                        if (in_array($question_id, $media_key_list)) {
7471
                            $question_id = $media_questions[$question_id];
7472
                        }
7473
                    }
7474
                    $new_question_list = array_flatten($new_question_list);
7475
                }
7476
            } else {
7477
                $new_question_list = $question_list;
7478
            }
7479
        }
7480
7481
        return $new_question_list;
7482
    }
7483
7484
    /**
7485
     * @param int $exe_id
7486
     *
7487
     * @return array
7488
     */
7489
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7490
    {
7491
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7492
        $exe_id = (int) $exe_id;
7493
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7494
        $result = Database::query($sql_track);
7495
        $new_array = [];
7496
        if (Database::num_rows($result) > 0) {
7497
            $new_array = Database::fetch_assoc($result);
7498
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7499
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7500
            $new_array['duration_formatted'] = '';
7501
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7502
                $time = api_format_time($new_array['exe_duration'], 'js');
7503
                $new_array['duration_formatted'] = $time;
7504
            }
7505
        }
7506
7507
        return $new_array;
7508
    }
7509
7510
    /**
7511
     * @param int $exeId
7512
     *
7513
     * @return bool
7514
     */
7515
    public function removeAllQuestionToRemind($exeId)
7516
    {
7517
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7518
        $exeId = (int) $exeId;
7519
        if (empty($exeId)) {
7520
            return false;
7521
        }
7522
        $sql = "UPDATE $table
7523
                SET questions_to_check = ''
7524
                WHERE exe_id = $exeId ";
7525
        Database::query($sql);
7526
7527
        return true;
7528
    }
7529
7530
    /**
7531
     * @param int   $exeId
7532
     * @param array $questionList
7533
     *
7534
     * @return bool
7535
     */
7536
    public function addAllQuestionToRemind($exeId, $questionList = [])
7537
    {
7538
        $exeId = (int) $exeId;
7539
        if (empty($questionList)) {
7540
            return false;
7541
        }
7542
7543
        $questionListToString = implode(',', $questionList);
7544
        $questionListToString = Database::escape_string($questionListToString);
7545
7546
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7547
        $sql = "UPDATE $table
7548
                SET questions_to_check = '$questionListToString'
7549
                WHERE exe_id = $exeId";
7550
        Database::query($sql);
7551
7552
        return true;
7553
    }
7554
7555
    /**
7556
     * @param int    $exeId
7557
     * @param int    $questionId
7558
     * @param string $action
7559
     */
7560
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7561
    {
7562
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7563
        $questionId = (int) $questionId;
7564
        $exeId = (int) $exeId;
7565
7566
        if ($exercise_info) {
7567
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7568
            if (empty($exercise_info['questions_to_check'])) {
7569
                if ('add' == $action) {
7570
                    $sql = "UPDATE $track_exercises
7571
                            SET questions_to_check = '$questionId'
7572
                            WHERE exe_id = $exeId ";
7573
                    Database::query($sql);
7574
                }
7575
            } else {
7576
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7577
                $remind_list_string = '';
7578
                if ('add' === $action) {
7579
                    if (!in_array($questionId, $remind_list)) {
7580
                        $newRemindList = [];
7581
                        $remind_list[] = $questionId;
7582
                        $questionListInSession = Session::read('questionList');
7583
                        if (!empty($questionListInSession)) {
7584
                            foreach ($questionListInSession as $originalQuestionId) {
7585
                                if (in_array($originalQuestionId, $remind_list)) {
7586
                                    $newRemindList[] = $originalQuestionId;
7587
                                }
7588
                            }
7589
                        }
7590
                        $remind_list_string = implode(',', $newRemindList);
7591
                    }
7592
                } elseif ('delete' == $action) {
7593
                    if (!empty($remind_list)) {
7594
                        if (in_array($questionId, $remind_list)) {
7595
                            $remind_list = array_flip($remind_list);
7596
                            unset($remind_list[$questionId]);
7597
                            $remind_list = array_flip($remind_list);
7598
7599
                            if (!empty($remind_list)) {
7600
                                sort($remind_list);
7601
                                array_filter($remind_list);
7602
                                $remind_list_string = implode(',', $remind_list);
7603
                            }
7604
                        }
7605
                    }
7606
                }
7607
                $value = Database::escape_string($remind_list_string);
7608
                $sql = "UPDATE $track_exercises
7609
                        SET questions_to_check = '$value'
7610
                        WHERE exe_id = $exeId ";
7611
                Database::query($sql);
7612
            }
7613
        }
7614
    }
7615
7616
    /**
7617
     * @param string $answer
7618
     */
7619
    public function fill_in_blank_answer_to_array($answer)
7620
    {
7621
        $list = null;
7622
        api_preg_match_all('/\[[^]]+\]/', $answer, $list);
7623
7624
        if (empty($list)) {
7625
            return '';
7626
        }
7627
7628
        return $list[0];
7629
    }
7630
7631
    /**
7632
     * @param string $answer
7633
     *
7634
     * @return string
7635
     */
7636
    public function fill_in_blank_answer_to_string($answer)
7637
    {
7638
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7639
        $result = '';
7640
        if (!empty($teacher_answer_list)) {
7641
            foreach ($teacher_answer_list as $teacher_item) {
7642
                //Cleaning student answer list
7643
                $value = strip_tags($teacher_item);
7644
                $value = api_substr($value, 1, api_strlen($value) - 2);
7645
                $value = explode('/', $value);
7646
                if (!empty($value[0])) {
7647
                    $value = trim($value[0]);
7648
                    $value = str_replace('&nbsp;', '', $value);
7649
                    $result .= $value;
7650
                }
7651
            }
7652
        }
7653
7654
        return $result;
7655
    }
7656
7657
    /**
7658
     * @return string
7659
     */
7660
    public function returnTimeLeftDiv()
7661
    {
7662
        $html = '<div id="clock_warning" style="display:none">';
7663
        $html .= Display::return_message(
7664
            get_lang('Time limit reached'),
7665
            'warning'
7666
        );
7667
        $html .= ' ';
7668
        $html .= sprintf(
7669
            get_lang('Just a moment, please. You will be redirected in %s seconds...'),
7670
            '<span id="counter_to_redirect" class="red_alert"></span>'
7671
        );
7672
        $html .= '</div>';
7673
        $icon = Display::getMdiIcon('clock-outline', 'ch-tool-icon');
7674
        $html .= '<div class="count_down">
7675
                    '.get_lang('Remaining time to finish exercise').'
7676
                    '.$icon.'<span id="exercise_clock_warning"></span>
7677
                </div>';
7678
7679
        return $html;
7680
    }
7681
7682
    /**
7683
     * Get categories added in the exercise--category matrix.
7684
     *
7685
     * @return array
7686
     */
7687
    public function getCategoriesInExercise()
7688
    {
7689
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7690
        if (!empty($this->getId())) {
7691
            $sql = "SELECT * FROM $table
7692
                    WHERE exercise_id = {$this->getId()} ";
7693
            $result = Database::query($sql);
7694
            $list = [];
7695
            if (Database::num_rows($result)) {
7696
                while ($row = Database::fetch_assoc($result)) {
7697
                    $list[$row['category_id']] = $row;
7698
                }
7699
7700
                return $list;
7701
            }
7702
        }
7703
7704
        return [];
7705
    }
7706
7707
    /**
7708
     * Get total number of question that will be parsed when using the category/exercise.
7709
     *
7710
     * @return int
7711
     */
7712
    public function getNumberQuestionExerciseCategory()
7713
    {
7714
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7715
        if (!empty($this->getId())) {
7716
            $sql = "SELECT SUM(count_questions) count_questions
7717
                    FROM $table
7718
                    WHERE exercise_id = {$this->getId()}";
7719
            $result = Database::query($sql);
7720
            if (Database::num_rows($result)) {
7721
                $row = Database::fetch_array($result);
7722
7723
                return (int) $row['count_questions'];
7724
            }
7725
        }
7726
7727
        return 0;
7728
    }
7729
7730
    /**
7731
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table
7732
     *
7733
     * @param array $categories
7734
     */
7735
    public function saveCategoriesInExercise($categories)
7736
    {
7737
        if (!empty($categories) && !empty($this->getId())) {
7738
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7739
            $sql = "DELETE FROM $table
7740
                    WHERE exercise_id = {$this->getId()}";
7741
            Database::query($sql);
7742
            foreach ($categories as $categoryId => $countQuestions) {
7743
                if ($categoryId !== 0) {
7744
                    $params = [
7745
                        'exercise_id' => $this->getId(),
7746
                        'category_id' => $categoryId,
7747
                        'count_questions' => $countQuestions,
7748
                    ];
7749
                    Database::insert($table, $params);
7750
                }
7751
            }
7752
        }
7753
    }
7754
7755
    /**
7756
     * @param array  $questionList
7757
     * @param int    $currentQuestion
7758
     * @param array  $conditions
7759
     * @param string $link
7760
     *
7761
     * @return string
7762
     */
7763
    public function progressExercisePaginationBar(
7764
        $questionList,
7765
        $currentQuestion,
7766
        $conditions,
7767
        $link
7768
    ) {
7769
        $mediaQuestions = $this->getMediaList();
7770
7771
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7772
        $counter = 0;
7773
        $nextValue = 0;
7774
        $wasMedia = false;
7775
        $before = 0;
7776
        $counterNoMedias = 0;
7777
        foreach ($questionList as $questionId) {
7778
            $isCurrent = $currentQuestion == $counterNoMedias + 1 ? true : false;
7779
7780
            if (!empty($nextValue)) {
7781
                if ($wasMedia) {
7782
                    $nextValue = $nextValue - $before + 1;
7783
                }
7784
            }
7785
7786
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7787
                $fixedValue = $counterNoMedias;
7788
7789
                $html .= Display::progressPaginationBar(
7790
                    $nextValue,
7791
                    $mediaQuestions[$questionId],
7792
                    $currentQuestion,
7793
                    $fixedValue,
7794
                    $conditions,
7795
                    $link,
7796
                    true,
7797
                    true
7798
                );
7799
7800
                $counter += count($mediaQuestions[$questionId]) - 1;
7801
                $before = count($questionList);
7802
                $wasMedia = true;
7803
                $nextValue += count($questionList);
7804
            } else {
7805
                $html .= Display::parsePaginationItem(
7806
                    $questionId,
7807
                    $isCurrent,
7808
                    $conditions,
7809
                    $link,
7810
                    $counter
7811
                );
7812
                $counter++;
7813
                $nextValue++;
7814
                $wasMedia = false;
7815
            }
7816
            $counterNoMedias++;
7817
        }
7818
        $html .= '</ul></div>';
7819
7820
        return $html;
7821
    }
7822
7823
    /**
7824
     *  Shows a list of numbers that represents the question to answer in a exercise.
7825
     *
7826
     * @param array  $categories
7827
     * @param int    $current
7828
     * @param array  $conditions
7829
     * @param string $link
7830
     *
7831
     * @return string
7832
     */
7833
    public function progressExercisePaginationBarWithCategories(
7834
        $categories,
7835
        $current,
7836
        $conditions = [],
7837
        $link = null
7838
    ) {
7839
        $html = null;
7840
        $counterNoMedias = 0;
7841
        $nextValue = 0;
7842
        $wasMedia = false;
7843
        $before = 0;
7844
7845
        if (!empty($categories)) {
7846
            $selectionType = $this->getQuestionSelectionType();
7847
            $useRootAsCategoryTitle = false;
7848
7849
            // Grouping questions per parent category see BT#6540
7850
            if (in_array(
7851
                $selectionType,
7852
                [
7853
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7854
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7855
                ]
7856
            )) {
7857
                $useRootAsCategoryTitle = true;
7858
            }
7859
7860
            // If the exercise is set to only show the titles of the categories
7861
            // at the root of the tree, then pre-order the categories tree by
7862
            // removing children and summing their questions into the parent
7863
            // categories
7864
            if ($useRootAsCategoryTitle) {
7865
                // The new categories list starts empty
7866
                $newCategoryList = [];
7867
                foreach ($categories as $category) {
7868
                    $rootElement = $category['root'];
7869
7870
                    if (isset($category['parent_info'])) {
7871
                        $rootElement = $category['parent_info']['id'];
7872
                    }
7873
7874
                    //$rootElement = $category['id'];
7875
                    // If the current category's ancestor was never seen
7876
                    // before, then declare it and assign the current
7877
                    // category to it.
7878
                    if (!isset($newCategoryList[$rootElement])) {
7879
                        $newCategoryList[$rootElement] = $category;
7880
                    } else {
7881
                        // If it was already seen, then merge the previous with
7882
                        // the current category
7883
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7884
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7885
                        $newCategoryList[$rootElement] = $category;
7886
                    }
7887
                }
7888
                // Now use the newly built categories list, with only parents
7889
                $categories = $newCategoryList;
7890
            }
7891
7892
            foreach ($categories as $category) {
7893
                $questionList = $category['question_list'];
7894
                // Check if in this category there questions added in a media
7895
                $mediaQuestionId = $category['media_question'];
7896
                $isMedia = false;
7897
                $fixedValue = null;
7898
7899
                // Media exists!
7900
                if (999 != $mediaQuestionId) {
7901
                    $isMedia = true;
7902
                    $fixedValue = $counterNoMedias;
7903
                }
7904
7905
                //$categoryName = $category['path']; << show the path
7906
                $categoryName = $category['name'];
7907
7908
                if ($useRootAsCategoryTitle) {
7909
                    if (isset($category['parent_info'])) {
7910
                        $categoryName = $category['parent_info']['title'];
7911
                    }
7912
                }
7913
                $html .= '<div class="row">';
7914
                $html .= '<div class="span2">'.$categoryName.'</div>';
7915
                $html .= '<div class="span8">';
7916
7917
                if (!empty($nextValue)) {
7918
                    if ($wasMedia) {
7919
                        $nextValue = $nextValue - $before + 1;
7920
                    }
7921
                }
7922
                $html .= Display::progressPaginationBar(
7923
                    $nextValue,
7924
                    $questionList,
7925
                    $current,
7926
                    $fixedValue,
7927
                    $conditions,
7928
                    $link,
7929
                    $isMedia,
7930
                    true
7931
                );
7932
                $html .= '</div>';
7933
                $html .= '</div>';
7934
7935
                if (999 == $mediaQuestionId) {
7936
                    $counterNoMedias += count($questionList);
7937
                } else {
7938
                    $counterNoMedias++;
7939
                }
7940
7941
                $nextValue += count($questionList);
7942
                $before = count($questionList);
7943
7944
                if (999 != $mediaQuestionId) {
7945
                    $wasMedia = true;
7946
                } else {
7947
                    $wasMedia = false;
7948
                }
7949
            }
7950
        }
7951
7952
        return $html;
7953
    }
7954
7955
    /**
7956
     * Renders a question list.
7957
     *
7958
     * @param array $questionList    (with media questions compressed)
7959
     * @param int   $currentQuestion
7960
     * @param array $exerciseResult
7961
     * @param array $attemptList
7962
     * @param array $remindList
7963
     */
7964
    public function renderQuestionList(
7965
        $questionList,
7966
        $currentQuestion,
7967
        $exerciseResult,
7968
        $attemptList,
7969
        $remindList
7970
    ) {
7971
        $mediaQuestions = $this->getMediaList();
7972
        $i = 0;
7973
7974
        // Normal question list render (medias compressed)
7975
        foreach ($questionList as $questionId) {
7976
            $i++;
7977
            // For sequential exercises
7978
7979
            if (ONE_PER_PAGE == $this->type) {
7980
                // If it is not the right question, goes to the next loop iteration
7981
                if ($currentQuestion != $i) {
7982
                    continue;
7983
                } else {
7984
                    if (!in_array(
7985
                        $this->getFeedbackType(),
7986
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7987
                    )) {
7988
                        // if the user has already answered this question
7989
                        if (isset($exerciseResult[$questionId])) {
7990
                            echo Display::return_message(
7991
                                get_lang('You already answered the question'),
7992
                                'normal'
7993
                            );
7994
7995
                            break;
7996
                        }
7997
                    }
7998
                }
7999
            }
8000
8001
            // The $questionList contains the media id we check
8002
            // if this questionId is a media question type
8003
            if (isset($mediaQuestions[$questionId]) &&
8004
                999 != $mediaQuestions[$questionId]
8005
            ) {
8006
                // The question belongs to a media
8007
                $mediaQuestionList = $mediaQuestions[$questionId];
8008
                $objQuestionTmp = Question::read($questionId);
8009
8010
                $counter = 1;
8011
                if (MEDIA_QUESTION == $objQuestionTmp->type) {
8012
                    echo $objQuestionTmp->show_media_content();
8013
8014
                    $countQuestionsInsideMedia = count($mediaQuestionList);
8015
8016
                    // Show questions that belongs to a media
8017
                    if (!empty($mediaQuestionList)) {
8018
                        // In order to parse media questions we use letters a, b, c, etc.
8019
                        $letterCounter = 97;
8020
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
8021
                            $isLastQuestionInMedia = false;
8022
                            if ($counter == $countQuestionsInsideMedia) {
8023
                                $isLastQuestionInMedia = true;
8024
                            }
8025
                            $this->renderQuestion(
8026
                                $questionIdInsideMedia,
8027
                                $attemptList,
8028
                                $remindList,
8029
                                chr($letterCounter),
8030
                                $currentQuestion,
8031
                                $mediaQuestionList,
8032
                                $isLastQuestionInMedia,
8033
                                $questionList
8034
                            );
8035
                            $letterCounter++;
8036
                            $counter++;
8037
                        }
8038
                    }
8039
                } else {
8040
                    $this->renderQuestion(
8041
                        $questionId,
8042
                        $attemptList,
8043
                        $remindList,
8044
                        $i,
8045
                        $currentQuestion,
8046
                        null,
8047
                        null,
8048
                        $questionList
8049
                    );
8050
                    $i++;
8051
                }
8052
            } else {
8053
                // Normal question render.
8054
                $this->renderQuestion(
8055
                    $questionId,
8056
                    $attemptList,
8057
                    $remindList,
8058
                    $i,
8059
                    $currentQuestion,
8060
                    null,
8061
                    null,
8062
                    $questionList
8063
                );
8064
            }
8065
8066
            // For sequential exercises.
8067
            if (ONE_PER_PAGE == $this->type) {
8068
                // quits the loop
8069
                break;
8070
            }
8071
        }
8072
        // end foreach()
8073
8074
        if (ALL_ON_ONE_PAGE == $this->type) {
8075
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
8076
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
8077
        }
8078
    }
8079
8080
    /**
8081
     * Not implemented in 1.11.x.
8082
     *
8083
     * @param int   $questionId
8084
     * @param array $attemptList
8085
     * @param array $remindList
8086
     * @param int   $i
8087
     * @param int   $current_question
8088
     * @param array $questions_in_media
8089
     * @param bool  $last_question_in_media
8090
     * @param array $realQuestionList
8091
     * @param bool  $generateJS
8092
     */
8093
    public function renderQuestion(
8094
        $questionId,
8095
        $attemptList,
8096
        $remindList,
8097
        $i,
8098
        $current_question,
8099
        $questions_in_media = [],
8100
        $last_question_in_media = false,
8101
        $realQuestionList = [],
8102
        $generateJS = true
8103
    ) {
8104
        // With this option on the question is loaded via AJAX
8105
        //$generateJS = true;
8106
        //$this->loadQuestionAJAX = true;
8107
8108
        if ($generateJS && $this->loadQuestionAJAX) {
8109
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
8110
            $params = [
8111
                'questionId' => $questionId,
8112
                'attemptList' => $attemptList,
8113
                'remindList' => $remindList,
8114
                'i' => $i,
8115
                'current_question' => $current_question,
8116
                'questions_in_media' => $questions_in_media,
8117
                'last_question_in_media' => $last_question_in_media,
8118
            ];
8119
            $params = json_encode($params);
8120
8121
            $script = '<script>
8122
            $(function(){
8123
                var params = '.$params.';
8124
                $.ajax({
8125
                    type: "GET",
8126
                    data: params,
8127
                    url: "'.$url.'",
8128
                    success: function(return_value) {
8129
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
8130
                    }
8131
                });
8132
            });
8133
            </script>
8134
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
8135
            echo $script;
8136
        } else {
8137
            $origin = api_get_origin();
8138
            $question_obj = Question::read($questionId);
8139
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
8140
            $remind_highlight = null;
8141
8142
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
8143
            // see #4542 no_remind_highlight class hide with jquery
8144
            if (ALL_ON_ONE_PAGE == $this->type && isset($_GET['reminder']) && 2 == $_GET['reminder']) {
8145
                $remind_highlight = 'no_remind_highlight';
8146
                // @todo not implemented in 1.11.x
8147
                /*if (in_array($question_obj->type, Question::question_type_no_review())) {
8148
                    return null;
8149
                }*/
8150
            }
8151
8152
            $attributes = ['id' => 'remind_list['.$questionId.']'];
8153
8154
            // Showing the question
8155
            $exercise_actions = null;
8156
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
8157
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
8158
8159
            // Shows the question + possible answers
8160
            $showTitle = 1 == $this->getHideQuestionTitle() ? false : true;
8161
            // @todo not implemented in 1.11.x
8162
            /*echo $this->showQuestion(
8163
                $question_obj,
8164
                false,
8165
                $origin,
8166
                $i,
8167
                $showTitle,
8168
                false,
8169
                $user_choice,
8170
                false,
8171
                null,
8172
                false,
8173
                $this->getModelType(),
8174
                $this->categoryMinusOne
8175
            );*/
8176
8177
            // Button save and continue
8178
            switch ($this->type) {
8179
                case ONE_PER_PAGE:
8180
                    $exercise_actions .= $this->show_button(
8181
                        $questionId,
8182
                        $current_question,
8183
                        null,
8184
                        $remindList
8185
                    );
8186
8187
                    break;
8188
                case ALL_ON_ONE_PAGE:
8189
                    if (api_is_allowed_to_session_edit()) {
8190
                        $button = [
8191
                            Display::button(
8192
                                'save_now',
8193
                                get_lang('Save and continue'),
8194
                                ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
8195
                            ),
8196
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
8197
                        ];
8198
                        $exercise_actions .= Display::div(
8199
                            implode(PHP_EOL, $button),
8200
                            ['class' => 'exercise_save_now_button mb-4']
8201
                        );
8202
                    }
8203
8204
                    break;
8205
            }
8206
8207
            if (!empty($questions_in_media)) {
8208
                $count_of_questions_inside_media = count($questions_in_media);
8209
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
8210
                    $button = [
8211
                        Display::button(
8212
                            'save_now',
8213
                            get_lang('Save and continue'),
8214
                            ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
8215
                        ),
8216
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
8217
                    ];
8218
                    $exercise_actions = Display::div(
8219
                        implode(PHP_EOL, $button),
8220
                        ['class' => 'exercise_save_now_button mb-4']
8221
                    );
8222
                }
8223
8224
                if ($last_question_in_media && ONE_PER_PAGE == $this->type) {
8225
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
8226
                }
8227
            }
8228
8229
            // Checkbox review answers. Not implemented.
8230
            /*if ($this->review_answers &&
8231
                !in_array($question_obj->type, Question::question_type_no_review())
8232
            ) {
8233
                $remind_question_div = Display::tag(
8234
                    'label',
8235
                    Display::input(
8236
                        'checkbox',
8237
                        'remind_list['.$questionId.']',
8238
                        '',
8239
                        $attributes
8240
                    ).get_lang('Revise question later'),
8241
                    [
8242
                        'class' => 'checkbox',
8243
                        'for' => 'remind_list['.$questionId.']',
8244
                    ]
8245
                );
8246
                $exercise_actions .= Display::div(
8247
                    $remind_question_div,
8248
                    ['class' => 'exercise_save_now_button']
8249
                );
8250
            }*/
8251
8252
            echo Display::div(' ', ['class' => 'clear']);
8253
8254
            $paginationCounter = null;
8255
            if (ONE_PER_PAGE == $this->type) {
8256
                if (empty($questions_in_media)) {
8257
                    $paginationCounter = Display::paginationIndicator(
8258
                        $current_question,
8259
                        count($realQuestionList)
8260
                    );
8261
                } else {
8262
                    if ($last_question_in_media) {
8263
                        $paginationCounter = Display::paginationIndicator(
8264
                            $current_question,
8265
                            count($realQuestionList)
8266
                        );
8267
                    }
8268
                }
8269
            }
8270
8271
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
8272
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
8273
            echo '</div>';
8274
        }
8275
    }
8276
8277
    /**
8278
     * Returns an array of categories details for the questions of the current
8279
     * exercise.
8280
     *
8281
     * @return array
8282
     */
8283
    public function getQuestionWithCategories()
8284
    {
8285
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
8286
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
8287
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8288
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
8289
        $sql = "SELECT DISTINCT cat.*
8290
                FROM $TBL_EXERCICE_QUESTION e
8291
                INNER JOIN $TBL_QUESTIONS q
8292
                ON (e.question_id = q.iid)
8293
                INNER JOIN $categoryRelTable catRel
8294
                ON (catRel.question_id = e.question_id)
8295
                INNER JOIN $categoryTable cat
8296
                ON (cat.iid = catRel.category_id)
8297
                WHERE
8298
                  e.quiz_id	= ".(int) ($this->getId());
8299
8300
        $result = Database::query($sql);
8301
        $categoriesInExercise = [];
8302
        if (Database::num_rows($result)) {
8303
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
8304
        }
8305
8306
        return $categoriesInExercise;
8307
    }
8308
8309
    /**
8310
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
8311
     */
8312
    public function getMaxScore()
8313
    {
8314
        $outMaxScore = 0;
8315
        // list of question's id !!! the array key start at 1 !!!
8316
        $questionList = $this->selectQuestionList(true);
8317
8318
        if ($this->random > 0 && $this->randomByCat > 0) {
8319
            // test is random by category
8320
            // get the $numberRandomQuestions best score question of each category
8321
            $numberRandomQuestions = $this->random;
8322
            $tabCategoriesScores = [];
8323
            foreach ($questionList as $questionId) {
8324
                $questionCategoryId = TestCategory::getCategoryForQuestion($questionId);
8325
                if (!is_array($tabCategoriesScores[$questionCategoryId])) {
8326
                    $tabCategoriesScores[$questionCategoryId] = [];
8327
                }
8328
                $tmpObjQuestion = Question::read($questionId);
8329
                if (is_object($tmpObjQuestion)) {
8330
                    $tabCategoriesScores[$questionCategoryId][] = $tmpObjQuestion->weighting;
8331
                }
8332
            }
8333
8334
            // here we've got an array with first key, the category_id, second key, score of question for this cat
8335
            foreach ($tabCategoriesScores as $tabScores) {
8336
                rsort($tabScores);
8337
                $tabScoresCount = count($tabScores);
8338
                for ($i = 0; $i < min($numberRandomQuestions, $tabScoresCount); $i++) {
8339
                    $outMaxScore += $tabScores[$i];
8340
                }
8341
            }
8342
8343
            return $outMaxScore;
8344
        }
8345
8346
        // standard test, just add each question score
8347
        foreach ($questionList as $questionId) {
8348
            $question = Question::read($questionId, $this->course);
8349
            $outMaxScore += $question->weighting;
8350
        }
8351
8352
        return $outMaxScore;
8353
    }
8354
8355
    /**
8356
     * @return string
8357
     */
8358
    public function get_formated_title()
8359
    {
8360
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
8361
        }
8362
8363
        return api_html_entity_decode($this->selectTitle());
8364
    }
8365
8366
    /**
8367
     * @param string $title
8368
     *
8369
     * @return string
8370
     */
8371
    public static function get_formated_title_variable($title)
8372
    {
8373
        return api_html_entity_decode($title);
8374
    }
8375
8376
    /**
8377
     * @return string
8378
     */
8379
    public function format_title()
8380
    {
8381
        return api_htmlentities($this->title);
8382
    }
8383
8384
    /**
8385
     * @param string $title
8386
     *
8387
     * @return string
8388
     */
8389
    public static function format_title_variable($title)
8390
    {
8391
        return api_htmlentities($title);
8392
    }
8393
8394
    /**
8395
     * @param int $courseId
8396
     * @param int $sessionId
8397
     *
8398
     * @return array exercises
8399
     */
8400
    public function getExercisesByCourseSession($courseId, $sessionId)
8401
    {
8402
        $courseId = (int) $courseId;
8403
        $sessionId = (int) $sessionId;
8404
8405
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
8406
        $sql = "SELECT * FROM $tbl_quiz cq
8407
                WHERE
8408
                    cq.c_id = %s AND
8409
                    (cq.session_id = %s OR cq.session_id = 0) AND
8410
                    cq.active = 0
8411
                ORDER BY cq.iid";
8412
        $sql = sprintf($sql, $courseId, $sessionId);
8413
8414
        $result = Database::query($sql);
8415
8416
        $rows = [];
8417
        while ($row = Database::fetch_assoc($result)) {
8418
            $rows[] = $row;
8419
        }
8420
8421
        return $rows;
8422
    }
8423
8424
    /**
8425
     * @param int   $courseId
8426
     * @param int   $sessionId
8427
     * @param array $quizId
8428
     *
8429
     * @return array exercises
8430
     */
8431
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
8432
    {
8433
        if (empty($quizId)) {
8434
            return [];
8435
        }
8436
8437
        $sessionId = (int) $sessionId;
8438
        $courseId = (int) $courseId;
8439
8440
        $ids = is_array($quizId) ? $quizId : [$quizId];
8441
        $ids = array_map('intval', $ids);
8442
        $ids = implode(',', $ids);
8443
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8444
        if (0 != $sessionId) {
8445
            $sql = "SELECT * FROM $track_exercises te
8446
              INNER JOIN c_quiz cq
8447
              ON cq.iid = te.exe_exo_id
8448
              WHERE
8449
              te.c_id = %d AND
8450
              te.session_id = %s AND
8451
              cq.iid IN (%s)
8452
              ORDER BY cq.iid";
8453
8454
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
8455
        } else {
8456
            $sql = "SELECT * FROM $track_exercises te
8457
              INNER JOIN c_quiz cq ON cq.iid = te.exe_exo_id
8458
              WHERE
8459
              te.c_id = %d AND
8460
              cq.iid IN (%s)
8461
              ORDER BY cq.iid";
8462
            $sql = sprintf($sql, $courseId, $ids);
8463
        }
8464
        $result = Database::query($sql);
8465
        $rows = [];
8466
        while ($row = Database::fetch_assoc($result)) {
8467
            $rows[] = $row;
8468
        }
8469
8470
        return $rows;
8471
    }
8472
8473
    /**
8474
     * @param $exeId
8475
     * @param $exercise_stat_info
8476
     * @param $remindList
8477
     * @param $currentQuestion
8478
     *
8479
     * @return int|null
8480
     */
8481
    public static function getNextQuestionId(
8482
        $exeId,
8483
        $exercise_stat_info,
8484
        $remindList,
8485
        $currentQuestion
8486
    ) {
8487
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8488
8489
        if (isset($result[$exeId])) {
8490
            $result = $result[$exeId];
8491
        } else {
8492
            return null;
8493
        }
8494
8495
        $data_tracking = $exercise_stat_info['data_tracking'];
8496
        $data_tracking = explode(',', $data_tracking);
8497
8498
        // if this is the final question do nothing.
8499
        if ($currentQuestion == count($data_tracking)) {
8500
            return null;
8501
        }
8502
8503
        $currentQuestion--;
8504
8505
        if (!empty($result['question_list'])) {
8506
            $answeredQuestions = [];
8507
            foreach ($result['question_list'] as $question) {
8508
                if (!empty($question['answer'])) {
8509
                    $answeredQuestions[] = $question['question_id'];
8510
                }
8511
            }
8512
8513
            // Checking answered questions
8514
            $counterAnsweredQuestions = 0;
8515
            foreach ($data_tracking as $questionId) {
8516
                if (!in_array($questionId, $answeredQuestions)) {
8517
                    if ($currentQuestion != $counterAnsweredQuestions) {
8518
                        break;
8519
                    }
8520
                }
8521
                $counterAnsweredQuestions++;
8522
            }
8523
8524
            $counterRemindListQuestions = 0;
8525
            // Checking questions saved in the reminder list
8526
            if (!empty($remindList)) {
8527
                foreach ($data_tracking as $questionId) {
8528
                    if (in_array($questionId, $remindList)) {
8529
                        // Skip the current question
8530
                        if ($currentQuestion != $counterRemindListQuestions) {
8531
                            break;
8532
                        }
8533
                    }
8534
                    $counterRemindListQuestions++;
8535
                }
8536
8537
                if ($counterRemindListQuestions < $currentQuestion) {
8538
                    return null;
8539
                }
8540
8541
                if (!empty($counterRemindListQuestions)) {
8542
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8543
                        return $counterAnsweredQuestions;
8544
                    } else {
8545
                        return $counterRemindListQuestions;
8546
                    }
8547
                }
8548
            }
8549
8550
            return $counterAnsweredQuestions;
8551
        }
8552
    }
8553
8554
    /**
8555
     * Gets the position of a questionId in the question list.
8556
     *
8557
     * @param $questionId
8558
     *
8559
     * @return int
8560
     */
8561
    public function getPositionInCompressedQuestionList($questionId)
8562
    {
8563
        $questionList = $this->getQuestionListWithMediasCompressed();
8564
        $mediaQuestions = $this->getMediaList();
8565
        $position = 1;
8566
        foreach ($questionList as $id) {
8567
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8568
                $mediaQuestionList = $mediaQuestions[$id];
8569
                if (in_array($questionId, $mediaQuestionList)) {
8570
                    return $position;
8571
                } else {
8572
                    $position++;
8573
                }
8574
            } else {
8575
                if ($id == $questionId) {
8576
                    return $position;
8577
                } else {
8578
                    $position++;
8579
                }
8580
            }
8581
        }
8582
8583
        return 1;
8584
    }
8585
8586
    /**
8587
     * Get the correct answers in all attempts.
8588
     *
8589
     * @param int  $learnPathId
8590
     * @param int  $learnPathItemId
8591
     * @param bool $onlyCorrect
8592
     *
8593
     * @return array
8594
     */
8595
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8596
    {
8597
        $attempts = Event::getExerciseResultsByUser(
8598
            api_get_user_id(),
8599
            $this->getId(),
8600
            api_get_course_int_id(),
8601
            api_get_session_id(),
8602
            $learnPathId,
8603
            $learnPathItemId,
8604
            'DESC'
8605
        );
8606
8607
        $list = [];
8608
        foreach ($attempts as $attempt) {
8609
            foreach ($attempt['question_list'] as $answers) {
8610
                foreach ($answers as $answer) {
8611
                    $objAnswer = new Answer($answer['question_id']);
8612
                    if ($onlyCorrect) {
8613
                        switch ($objAnswer->getQuestionType()) {
8614
                            case FILL_IN_BLANKS:
8615
                            case FILL_IN_BLANKS_COMBINATION:
8616
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8617
8618
                                break;
8619
                            case MATCHING:
8620
                            case MATCHING_COMBINATION:
8621
                            case DRAGGABLE:
8622
                            case MATCHING_DRAGGABLE:
8623
                            case MATCHING_DRAGGABLE_COMBINATION:
8624
                                $isCorrect = Matching::isCorrect(
8625
                                    $answer['position'],
8626
                                    $answer['answer'],
8627
                                    $answer['question_id']
8628
                                );
8629
8630
                                break;
8631
                            case ORAL_EXPRESSION:
8632
                                $isCorrect = false;
8633
8634
                                break;
8635
                            default:
8636
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8637
                        }
8638
                        if ($isCorrect) {
8639
                            $list[$answer['question_id']][] = $answer;
8640
                        }
8641
                    } else {
8642
                        $list[$answer['question_id']][] = $answer;
8643
                    }
8644
                }
8645
            }
8646
8647
            if (false === $onlyCorrect) {
8648
                // Only take latest attempt
8649
                break;
8650
            }
8651
        }
8652
8653
        return $list;
8654
    }
8655
8656
    /**
8657
     * Get the correct answers in all attempts.
8658
     *
8659
     * @param int $learnPathId
8660
     * @param int $learnPathItemId
8661
     *
8662
     * @return array
8663
     */
8664
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8665
    {
8666
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8667
    }
8668
8669
    /**
8670
     * @return bool
8671
     */
8672
    public function showPreviousButton()
8673
    {
8674
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
8675
        if (false === $allow) {
8676
            return true;
8677
        }
8678
8679
        return $this->showPreviousButton;
8680
    }
8681
8682
    public function getPreventBackwards()
8683
    {
8684
        return (int) $this->preventBackwards;
8685
    }
8686
8687
    /**
8688
     * @return int
8689
     */
8690
    public function getQuizCategoryId(): ?int
8691
    {
8692
        if (empty($this->quizCategoryId)) {
8693
            return null;
8694
        }
8695
8696
        return (int) $this->quizCategoryId;
8697
    }
8698
8699
    /**
8700
     * @param int $value
8701
     */
8702
    public function setQuizCategoryId($value): void
8703
    {
8704
        if (!empty($value)) {
8705
            $this->quizCategoryId = (int) $value;
8706
        }
8707
    }
8708
8709
    /**
8710
     * Set the value to 1 to hide the question number.
8711
     *
8712
     * @param int $value
8713
     */
8714
    public function setHideQuestionNumber($value = 0)
8715
    {
8716
        $this->hideQuestionNumber = (int) $value;
8717
    }
8718
8719
    /**
8720
     * Gets the value to hide or show the question number. If it does not exist, it is set to 0.
8721
     *
8722
     * @return int 1 if the question number must be hidden
8723
     */
8724
    public function getHideQuestionNumber()
8725
    {
8726
        return (int) $this->hideQuestionNumber;
8727
    }
8728
8729
    public function setPageResultConfiguration(array $values)
8730
    {
8731
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8732
        if ($pageConfig) {
8733
            $params = [
8734
                'hide_expected_answer' => $values['hide_expected_answer'] ?? '',
8735
                'hide_question_score' => $values['hide_question_score'] ?? '',
8736
                'hide_total_score' => $values['hide_total_score'] ?? '',
8737
                'hide_category_table' => $values['hide_category_table'] ?? '',
8738
                'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
8739
            ];
8740
            $this->pageResultConfiguration = $params;
8741
        }
8742
    }
8743
8744
    /**
8745
     * @param array $defaults
8746
     */
8747
    public function setPageResultConfigurationDefaults(&$defaults)
8748
    {
8749
        $configuration = $this->getPageResultConfiguration();
8750
        if (!empty($configuration) && !empty($defaults)) {
8751
            $defaults = array_merge($defaults, $configuration);
8752
        }
8753
    }
8754
8755
    /**
8756
     * @return array
8757
     */
8758
    public function getPageResultConfiguration()
8759
    {
8760
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8761
        if ($pageConfig) {
8762
            return $this->pageResultConfiguration;
8763
        }
8764
8765
        return [];
8766
    }
8767
8768
    /**
8769
     * @param string $attribute
8770
     *
8771
     * @return mixed|null
8772
     */
8773
    public function getPageConfigurationAttribute($attribute)
8774
    {
8775
        $result = $this->getPageResultConfiguration();
8776
8777
        if (!empty($result)) {
8778
            return $result[$attribute] ?? null;
8779
        }
8780
8781
        return null;
8782
    }
8783
8784
    /**
8785
     * @param bool $showPreviousButton
8786
     *
8787
     * @return Exercise
8788
     */
8789
    public function setShowPreviousButton($showPreviousButton)
8790
    {
8791
        $this->showPreviousButton = $showPreviousButton;
8792
8793
        return $this;
8794
    }
8795
8796
    /**
8797
     * @param array $notifications
8798
     */
8799
    public function setNotifications($notifications)
8800
    {
8801
        $this->notifications = $notifications;
8802
    }
8803
8804
    /**
8805
     * @return array
8806
     */
8807
    public function getNotifications()
8808
    {
8809
        return $this->notifications;
8810
    }
8811
8812
    /**
8813
     * @return bool
8814
     */
8815
    public function showExpectedChoice()
8816
    {
8817
        return ('true' === api_get_setting('exercise.show_exercise_expected_choice'));
8818
    }
8819
8820
    /**
8821
     * @return bool
8822
     */
8823
    public function showExpectedChoiceColumn()
8824
    {
8825
        if (true === $this->forceShowExpectedChoiceColumn) {
8826
            return true;
8827
        }
8828
        if ($this->hideExpectedAnswer) {
8829
            return false;
8830
        }
8831
        if (!in_array(
8832
            $this->results_disabled,
8833
            [
8834
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8835
            ]
8836
        )
8837
        ) {
8838
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8839
            if (1 === $hide) {
8840
                return false;
8841
            }
8842
8843
            return true;
8844
        }
8845
8846
        return false;
8847
    }
8848
8849
    public function getQuestionRibbon(string $class, string $scoreLabel, ?string $result, array $array): string
8850
    {
8851
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8852
        if (1 === $hide) {
8853
            return '';
8854
        }
8855
8856
        $ribbon = '<div class="question-answer-result__header-ribbon-title question-answer-result__header-ribbon-title--'.$class.'">'.$scoreLabel.'</div>';
8857
        if (!empty($result)) {
8858
            $ribbon .= '<div class="question-answer-result__header-ribbon-detail">'
8859
                .get_lang('Score').': '.$result
8860
                .'</div>';
8861
        }
8862
8863
        $ribbonClassModifier = '';
8864
8865
        if ($this->showExpectedChoice()) {
8866
            $hideLabel = ('true' === api_get_setting('exercise.exercise_hide_label'));
8867
            if (true === $hideLabel) {
8868
                $ribbonClassModifier = 'question-answer-result__header-ribbon--no-ribbon';
8869
                $html = '';
8870
                $answerUsed = (int) $array['used'];
8871
                $answerMissing = (int) $array['missing'] - $answerUsed;
8872
                for ($i = 1; $i <= $answerUsed; $i++) {
8873
                    $html .= Display::getMdiIcon(StateIcon::COMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8874
                }
8875
                for ($i = 1; $i <= $answerMissing; $i++) {
8876
                    $html .= Display::getMdiIcon(StateIcon::INCOMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8877
                }
8878
                $ribbon = '<div class="question-answer-result__header-ribbon-title hide-label-title">'
8879
                    .get_lang('Correct answers').': '.$result.'</div>'
8880
                    .'<div class="question-answer-result__header-ribbon-detail">'.$html.'</div>';
8881
            }
8882
        }
8883
8884
        return Display::div(
8885
            $ribbon,
8886
            ['class' => "question-answer-result__header-ribbon $ribbonClassModifier"]
8887
        );
8888
    }
8889
8890
    /**
8891
     * @return int
8892
     */
8893
    public function getAutoLaunch()
8894
    {
8895
        return $this->autolaunch;
8896
    }
8897
8898
    /**
8899
     * Clean auto launch settings for all exercise in course/course-session.
8900
     */
8901
    public function enableAutoLaunch()
8902
    {
8903
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8904
        $sql = "UPDATE $table SET autolaunch = 1
8905
                WHERE iid = ".$this->iId;
8906
        Database::query($sql);
8907
    }
8908
8909
    /**
8910
     * Clean auto launch settings for all exercise in course/course-session.
8911
     */
8912
    public function cleanCourseLaunchSettings()
8913
    {
8914
        $em = Database::getManager();
8915
8916
        $repo = Container::getQuizRepository();
8917
8918
        $session = api_get_session_entity();
8919
        $course = api_get_course_entity();
8920
8921
        $qb = $repo->getResourcesByCourse($course, $session);
8922
        $quizzes = $qb->getQuery()->getResult();
8923
8924
        foreach ($quizzes as $quiz) {
8925
            $quiz->setAutoLaunch(false);
8926
            $em->persist($quiz);
8927
        }
8928
8929
        $em->flush();
8930
    }
8931
8932
    /**
8933
     * Get the title without HTML tags.
8934
     *
8935
     * @return string
8936
     */
8937
    public function getUnformattedTitle()
8938
    {
8939
        return strip_tags(api_html_entity_decode($this->title));
8940
    }
8941
8942
    /**
8943
     * Get the question IDs from quiz_rel_question for the current quiz,
8944
     * using the parameters as the arguments to the SQL's LIMIT clause.
8945
     * Because the exercise_id is known, it also comes with a filter on
8946
     * the session, so sessions are not specified here.
8947
     *
8948
     * @param int $start  At which question do we want to start the list
8949
     * @param int $length Up to how many results we want
8950
     *
8951
     * @return array A list of question IDs
8952
     */
8953
    public function getQuestionForTeacher($start = 0, $length = 10)
8954
    {
8955
        $start = (int) $start;
8956
        if ($start < 0) {
8957
            $start = 0;
8958
        }
8959
8960
        $length = (int) $length;
8961
8962
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8963
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8964
        $sql = "SELECT DISTINCT e.question_id
8965
                FROM $quizRelQuestion e
8966
                INNER JOIN $question q
8967
                ON (e.question_id = q.iid)
8968
                WHERE
8969
8970
                    e.quiz_id = '".$this->getId()."'
8971
                ORDER BY question_order
8972
                LIMIT $start, $length
8973
            ";
8974
        $result = Database::query($sql);
8975
        $questionList = [];
8976
        while ($object = Database::fetch_object($result)) {
8977
            $questionList[] = $object->question_id;
8978
        }
8979
8980
        return $questionList;
8981
    }
8982
8983
    /**
8984
     * @param int   $exerciseId
8985
     * @param array $courseInfo
8986
     * @param int   $sessionId
8987
     *
8988
     * @return bool
8989
     */
8990
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8991
    {
8992
        $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
8993
        if (!$allowStats) {
8994
            return false;
8995
        }
8996
8997
        if (empty($courseInfo)) {
8998
            return false;
8999
        }
9000
9001
        $courseId = $courseInfo['real_id'];
9002
9003
        $sessionId = (int) $sessionId;
9004
        $exerciseId = (int) $exerciseId;
9005
9006
        $result = $this->read($exerciseId);
9007
9008
        if (empty($result)) {
9009
            api_not_allowed(true);
9010
        }
9011
9012
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
9013
9014
        $studentList = CourseManager::get_user_list_from_course_code(
9015
            $courseInfo['code'],
9016
            $sessionId,
9017
            null,
9018
            null,
9019
            $statusToFilter
9020
        );
9021
9022
        if (empty($studentList)) {
9023
            Display::addFlash(Display::return_message(get_lang('No users in course')));
9024
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
9025
            exit;
9026
        }
9027
9028
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9029
9030
        $studentIdList = [];
9031
        if (!empty($studentList)) {
9032
            $studentIdList = array_column($studentList, 'user_id');
9033
        }
9034
9035
        $sessionCondition = api_get_session_condition($sessionId);
9036
        if (false == $this->exercise_was_added_in_lp) {
9037
            $sql = "SELECT * FROM $tblStats
9038
                        WHERE
9039
                            exe_exo_id = $exerciseId AND
9040
                            orig_lp_id = 0 AND
9041
                            orig_lp_item_id = 0 AND
9042
                            status <> 'incomplete' AND
9043
                            c_id = $courseId
9044
                            $sessionCondition
9045
                        ";
9046
        } else {
9047
            $lpId = null;
9048
            if (!empty($this->lpList)) {
9049
                // Taking only the first LP
9050
                $lpId = $this->getLpBySession($sessionId);
9051
                $lpId = $lpId['lp_id'];
9052
            }
9053
9054
            $sql = "SELECT *
9055
                        FROM $tblStats
9056
                        WHERE
9057
                            exe_exo_id = $exerciseId AND
9058
                            orig_lp_id = $lpId AND
9059
                            status <> 'incomplete' AND
9060
                            session_id = $sessionId AND
9061
                            c_id = $courseId ";
9062
        }
9063
9064
        $sql .= ' ORDER BY exe_id DESC';
9065
9066
        $studentCount = 0;
9067
        $sum = 0;
9068
        $bestResult = 0;
9069
        $sumResult = 0;
9070
        $result = Database::query($sql);
9071
        while ($data = Database::fetch_assoc($result)) {
9072
            // Only take into account users in the current student list.
9073
            if (!empty($studentIdList)) {
9074
                if (!in_array($data['exe_user_id'], $studentIdList)) {
9075
                    continue;
9076
                }
9077
            }
9078
9079
            if (!isset($students[$data['exe_user_id']])) {
9080
                if (0 != $data['max_score']) {
9081
                    $students[$data['exe_user_id']] = $data['score'];
9082
                    if ($data['score'] > $bestResult) {
9083
                        $bestResult = $data['score'];
9084
                    }
9085
                    $sumResult += $data['score'];
9086
                }
9087
            }
9088
        }
9089
9090
        $count = count($studentList);
9091
        $average = $sumResult / $count;
9092
        $em = Database::getManager();
9093
9094
        $links = AbstractLink::getGradebookLinksFromItem(
9095
            $this->getId(),
9096
            LINK_EXERCISE,
9097
            $courseInfo['real_id'],
9098
            $sessionId
9099
        );
9100
9101
        if (empty($links)) {
9102
            $links = AbstractLink::getGradebookLinksFromItem(
9103
                $this->iId,
9104
                LINK_EXERCISE,
9105
                $courseInfo['real_id'],
9106
                $sessionId
9107
            );
9108
        }
9109
9110
        if (!empty($links)) {
9111
            $repo = $em->getRepository(GradebookLink::class);
9112
9113
            foreach ($links as $link) {
9114
                $linkId = $link['id'];
9115
                /** @var GradebookLink $exerciseLink */
9116
                $exerciseLink = $repo->find($linkId);
9117
                if ($exerciseLink) {
9118
                    $exerciseLink
9119
                        ->setUserScoreList($students)
9120
                        ->setBestScore($bestResult)
9121
                        ->setAverageScore($average)
9122
                        ->setScoreWeight($this->getMaxScore());
9123
                    $em->persist($exerciseLink);
9124
                    $em->flush();
9125
                }
9126
            }
9127
        }
9128
    }
9129
9130
    /**
9131
     * Return an HTML table of exercises for on-screen printing, including
9132
     * action icons. If no exercise is present and the user can edit the
9133
     * course, show a "create test" button.
9134
     *
9135
     * @param int    $categoryId
9136
     * @param string $keyword
9137
     * @param int    $userId
9138
     * @param int    $courseId
9139
     * @param int    $sessionId
9140
     * @param bool   $returnData
9141
     * @param int    $minCategoriesInExercise
9142
     * @param int    $filterByResultDisabled
9143
     * @param int    $filterByAttempt
9144
     *
9145
     * @return string|SortableTableFromArrayConfig
9146
     */
9147
    public static function exerciseGridResource(
9148
        $categoryId,
9149
        $keyword = '',
9150
        $userId = 0,
9151
        $courseId = 0,
9152
        $sessionId = 0,
9153
        $returnData = false,
9154
        $minCategoriesInExercise = 0,
9155
        $filterByResultDisabled = 0,
9156
        $filterByAttempt = 0,
9157
        $myActions = null,
9158
        $returnTable = false
9159
    ) {
9160
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
9161
        $courseId = $courseId ? (int) $courseId : api_get_course_int_id();
9162
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
9163
9164
        $course = api_get_course_entity($courseId);
9165
        $session = api_get_session_entity($sessionId);
9166
9167
        $userId = $userId ? (int) $userId : api_get_user_id();
9168
        $user = api_get_user_entity($userId);
9169
9170
        $repo = Container::getQuizRepository();
9171
9172
        $trackEExerciseRepo = Container::getTrackEExerciseRepository();
9173
        $pendingCorrections = $trackEExerciseRepo->getPendingCorrectionsByExercise($courseId);
9174
        $pendingAttempts = [];
9175
        foreach ($pendingCorrections as $correction) {
9176
            $pendingAttempts[$correction['exerciseId']] = $correction['pendingCount'];
9177
        }
9178
9179
        // 2. Get query builder from repo.
9180
        $qb = $repo->getResourcesByCourse($course, $session);
9181
9182
        if (!empty($categoryId)) {
9183
            $qb->andWhere($qb->expr()->eq('resource.quizCategory', $categoryId));
9184
        } else {
9185
            $qb->andWhere($qb->expr()->isNull('resource.quizCategory'));
9186
        }
9187
9188
        $allowDelete = self::allowAction('delete');
9189
        $allowClean = self::allowAction('clean_results');
9190
9191
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9192
9193
        $categoryId = (int) $categoryId;
9194
        $keyword = Database::escape_string($keyword);
9195
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
9196
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
9197
9198
        $courseId = $course->getId();
9199
        $tableRows = [];
9200
        $origin = api_get_origin();
9201
        $charset = 'utf-8';
9202
        $token = Security::get_token();
9203
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, ['real_id' => $courseId]);
9204
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
9205
        $content = '';
9206
        $column = 0;
9207
        if ($is_allowedToEdit) {
9208
            $column = 1;
9209
        }
9210
9211
        $table = new SortableTableFromArrayConfig(
9212
            [],
9213
            $column,
9214
            self::PAGINATION_ITEMS_PER_PAGE,
9215
            'exercises_cat_'.$categoryId.'_'.api_get_course_int_id().'_'.api_get_session_id()
9216
        );
9217
9218
        $limit = $table->per_page;
9219
        $page = $table->page_nr;
9220
        $from = $limit * ($page - 1);
9221
9222
        $categoryCondition = '';
9223
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
9224
            if (!empty($categoryId)) {
9225
                $categoryCondition = " AND quiz_category_id = $categoryId ";
9226
            } else {
9227
                $categoryCondition = ' AND quiz_category_id IS NULL ';
9228
            }
9229
        }
9230
9231
        if (!empty($keyword)) {
9232
            $qb->andWhere($qb->expr()->like('resource.title', ':keyword'));
9233
            $qb->setParameter('keyword', '%'.$keyword.'%');
9234
        }
9235
9236
        // Only for administrators
9237
        if ($is_allowedToEdit) {
9238
            $qb->andWhere($qb->expr()->neq('resource.active', -1));
9239
        } else {
9240
            $qb->andWhere($qb->expr()->eq('resource.active', 1));
9241
        }
9242
9243
        $qb->setFirstResult($from);
9244
        $qb->setMaxResults($limit);
9245
9246
        $filterByResultDisabledCondition = '';
9247
        $filterByResultDisabled = (int) $filterByResultDisabled;
9248
        if (!empty($filterByResultDisabled)) {
9249
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
9250
        }
9251
        $filterByAttemptCondition = '';
9252
        $filterByAttempt = (int) $filterByAttempt;
9253
        if (!empty($filterByAttempt)) {
9254
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
9255
        }
9256
9257
        $exerciseList = $qb->getQuery()->getResult();
9258
9259
        $total = $repo->getCount($qb);
9260
9261
        $webPath = api_get_path(WEB_CODE_PATH);
9262
        if (!empty($exerciseList)) {
9263
            $visibilitySetting = ('true' === api_get_setting('lp.show_hidden_exercise_added_to_lp'));
9264
            //avoid sending empty parameters
9265
            $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
9266
            $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
9267
9268
            /** @var CQuiz $exerciseEntity */
9269
            foreach ($exerciseList as $exerciseEntity) {
9270
                $currentRow = [];
9271
                $exerciseId = $exerciseEntity->getIid();
9272
                $actions = '';
9273
                $attempt_text = '';
9274
                $exercise = new Exercise($courseId);
9275
                $exercise->read($exerciseId, false);
9276
9277
                if (empty($exercise->iId)) {
9278
                    continue;
9279
                }
9280
9281
                $sessionId = api_get_session_id();
9282
                $allowToEditBaseCourse = true;
9283
                $visibility = $visibilityInCourse = $exerciseEntity->isVisible($course);
9284
                $visibilityInSession = false;
9285
                if (!empty($sessionId)) {
9286
                    // If we are in a session, the test is invisible
9287
                    // in the base course, it is included in a LP
9288
                    // *and* the setting to show it is *not*
9289
                    // specifically set to true, then hide it.
9290
                    if (false === $visibility) {
9291
                        if (!$visibilitySetting) {
9292
                            if ($exercise->exercise_was_added_in_lp) {
9293
                                continue;
9294
                            }
9295
                        }
9296
                    }
9297
9298
                    $visibility = $visibilityInSession = $exerciseEntity->isVisible($course, $session);
9299
                }
9300
9301
                // Validation when belongs to a session
9302
                $isBaseCourseExercise = true;
9303
                if (!($visibilityInCourse && $visibilityInSession)) {
9304
                    $isBaseCourseExercise = false;
9305
                }
9306
9307
                if (!empty($sessionId) && $isBaseCourseExercise) {
9308
                    $allowToEditBaseCourse = false;
9309
                }
9310
9311
                $resourceLink = $exerciseEntity->getFirstResourceLink();
9312
                if ($resourceLink && !$sessionId && $resourceLink->getSession() === null) {
9313
                    $allowToEditBaseCourse = true;
9314
                }
9315
9316
                $allowToEditSession = ($resourceLink && $resourceLink->getSession() && $resourceLink->getSession()->getId() === $sessionId);
9317
                $sessionStar = null;
9318
                if ($allowToEditSession) {
9319
                    $sessionStar = api_get_session_image($sessionId, $user);
9320
                }
9321
9322
                $locked = $exercise->is_gradebook_locked;
9323
9324
                $startTime = $exerciseEntity->getStartTime();
9325
                $endTime = $exerciseEntity->getEndTime();
9326
                $time_limits = false;
9327
                if (!empty($startTime) || !empty($endTime)) {
9328
                    $time_limits = true;
9329
                }
9330
9331
                $is_actived_time = false;
9332
                if ($time_limits) {
9333
                    // check if start time
9334
                    $start_time = false;
9335
                    if (!empty($startTime)) {
9336
                        $start_time = api_strtotime($startTime->format('Y-m-d H:i:s'), 'UTC');
9337
                    }
9338
                    $end_time = false;
9339
                    if (!empty($endTime)) {
9340
                        $end_time = api_strtotime($endTime->format('Y-m-d H:i:s'), 'UTC');
9341
                    }
9342
                    $now = time();
9343
                    //If both "clocks" are enable
9344
                    if ($start_time && $end_time) {
9345
                        if ($now > $start_time && $end_time > $now) {
9346
                            $is_actived_time = true;
9347
                        }
9348
                    } else {
9349
                        //we check the start and end
9350
                        if ($start_time) {
9351
                            if ($now > $start_time) {
9352
                                $is_actived_time = true;
9353
                            }
9354
                        }
9355
                        if ($end_time) {
9356
                            if ($end_time > $now) {
9357
                                $is_actived_time = true;
9358
                            }
9359
                        }
9360
                    }
9361
                }
9362
9363
                $cut_title = $exercise->getCutTitle();
9364
                $alt_title = '';
9365
                if ($cut_title != $exerciseEntity->getTitle()) {
9366
                    $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
9367
                }
9368
9369
                // Teacher only.
9370
                if ($is_allowedToEdit) {
9371
                    $lp_blocked = null;
9372
                    if (true == $exercise->exercise_was_added_in_lp) {
9373
                        $lp_blocked = Display::div(
9374
                            get_lang(
9375
                                '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.'
9376
                            ),
9377
                            ['class' => 'lp_content_type_label']
9378
                        );
9379
                    }
9380
9381
                    $style = '';
9382
                    if (!$visibility) {
9383
                        $style = 'color:grey';
9384
                    }
9385
9386
                    $title = $cut_title;
9387
9388
                    $url = '<a
9389
                        '.$alt_title.'
9390
                        id="tooltip_'.$exerciseId.'"
9391
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'"
9392
                        style = "'.$style.';float:left;"
9393
                        >
9394
                         '.Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, ICON_SIZE_SMALL, $title).$title.
9395
                        '</a>'.$sessionStar;
9396
9397
                    if (ExerciseLib::isQuizEmbeddable($exerciseEntity)) {
9398
                        $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'));
9399
                        $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9400
                    }
9401
9402
                    $pendingCount = $pendingAttempts[$exerciseId] ?? 0;
9403
                    if ($pendingCount > 0) {
9404
                        $pendingIcon = Display::getMdiIcon(
9405
                            ActionIcon::ALERT->value,
9406
                            'ch-tool-icon',
9407
                            null,
9408
                            ICON_SIZE_SMALL,
9409
                            get_lang('Pending attempts') . ": $pendingCount"
9410
                        );
9411
                        $url .= " $pendingIcon";
9412
                    }
9413
9414
                    $currentRow['title'] = $url.$lp_blocked;
9415
                    $rowi = $exerciseEntity->getQuestions()->count();
9416
                    if ($allowToEditBaseCourse || $allowToEditSession) {
9417
                        // Questions list
9418
                        $actions = Display::url(
9419
                            Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')),
9420
                            'admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9421
                        );
9422
9423
                        // Test settings
9424
                        $settings = Display::url(
9425
                            Display::getMdiIcon(ToolIcon::SETTINGS, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Configure')),
9426
                            'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9427
                        );
9428
9429
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9430
                            $settings = '';
9431
                        }
9432
                        $actions .= $settings;
9433
9434
                        // Exercise results
9435
                        $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9436
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9437
9438
                        if ($limitTeacherAccess) {
9439
                            if (api_is_platform_admin()) {
9440
                                $actions .= $resultsLink;
9441
                            }
9442
                        } else {
9443
                            // Exercise results
9444
                            $actions .= $resultsLink;
9445
                        }
9446
9447
                        // Auto launch
9448
                        $autoLaunch = $exercise->getAutoLaunch();
9449
                        if (empty($autoLaunch)) {
9450
                            $actions .= Display::url(
9451
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('Enable')),
9452
                                'exercise.php?'.api_get_cidreq(
9453
                                ).'&action=enable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9454
                            );
9455
                        } else {
9456
                            $actions .= Display::url(
9457
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Disable')),
9458
                                'exercise.php?'.api_get_cidreq(
9459
                                ).'&action=disable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9460
                            );
9461
                        }
9462
9463
                        // Export
9464
                        $actions .= Display::url(
9465
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9466
                            '',
9467
                            [
9468
                                'onclick' => "javascript:if(!confirm('".addslashes(
9469
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9470
                                    )." ".addslashes($title)."?"."')) return false;",
9471
                                'href' => 'exercise.php?'.api_get_cidreq(
9472
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9473
                            ]
9474
                        );
9475
9476
                        // Clean exercise
9477
                        $clean = '';
9478
                        if (true === $allowClean) {
9479
                            if (!$locked) {
9480
                                $clean = Display::url(
9481
                                    Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Clear all learners results for this exercise')
9482
                                    ),
9483
                                    '',
9484
                                    [
9485
                                        'onclick' => "javascript:if(!confirm('".
9486
                                            addslashes(
9487
                                                api_htmlentities(
9488
                                                    get_lang('Are you sure to delete results'),
9489
                                                    ENT_QUOTES
9490
                                                )
9491
                                            )." ".addslashes($title)."?"."')) return false;",
9492
                                        'href' => 'exercise.php?'.api_get_cidreq(
9493
                                            ).'&action=clean_results&sec_token='.$token.'&exerciseId='.$exerciseId,
9494
                                    ]
9495
                                );
9496
                            } else {
9497
                                $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.')
9498
                                );
9499
                            }
9500
                        }
9501
9502
                        $actions .= $clean;
9503
                        // Visible / invisible
9504
                        // Check if this exercise was added in a LP
9505
                        $visibility = '';
9506
                        if (api_is_platform_admin()) {
9507
                            if ($exercise->exercise_was_added_in_lp) {
9508
                                $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.')
9509
                                );
9510
                            } else {
9511
                                if (!$exerciseEntity->isVisible($course, $session)) {
9512
                                    $visibility = Display::url(
9513
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9514
                                        ),
9515
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9516
                                    );
9517
                                } else {
9518
                                    // else if not active
9519
                                    $visibility = Display::url(
9520
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9521
                                        ),
9522
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9523
                                    );
9524
                                }
9525
                            }
9526
                        }
9527
9528
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9529
                            $visibility = '';
9530
                        }
9531
9532
                        $actions .= $visibility;
9533
9534
                        // Export qti ...
9535
                        $export = Display::url(
9536
                            Display::getMdiIcon(
9537
                                'database',
9538
                                'ch-tool-icon',
9539
                                null,
9540
                                ICON_SIZE_SMALL,
9541
                                'IMS/QTI'
9542
                            ),
9543
                            'exercise.php?action=exportqti2&exerciseId='.$exerciseId.'&'.api_get_cidreq()
9544
                        );
9545
9546
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9547
                            $export = '';
9548
                        }
9549
9550
                        $actions .= $export;
9551
                    } else {
9552
                        // not session
9553
                        $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")
9554
                        );
9555
9556
                        // Check if this exercise was added in a LP
9557
                        $visibility = '';
9558
                        if (api_is_platform_admin()) {
9559
                            if ($exercise->exercise_was_added_in_lp) {
9560
                                $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.')
9561
                                );
9562
                            } else {
9563
                                if (0 === $exerciseEntity->getActive() || 0 == $visibility) {
9564
                                    $visibility = Display::url(
9565
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9566
                                        ),
9567
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9568
                                    );
9569
                                } else {
9570
                                    // else if not active
9571
                                    $visibility = Display::url(
9572
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9573
                                        ),
9574
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9575
                                    );
9576
                                }
9577
                            }
9578
                        }
9579
9580
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9581
                            $visibility = '';
9582
                        }
9583
9584
                        $actions .= $visibility;
9585
                        $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9586
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9587
                        $actions .= Display::url(
9588
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9589
                            '',
9590
                            [
9591
                                'onclick' => "javascript:if(!confirm('".addslashes(
9592
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9593
                                    )." ".addslashes($title)."?"."')) return false;",
9594
                                'href' => 'exercise.php?'.api_get_cidreq(
9595
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9596
                            ]
9597
                        );
9598
                    }
9599
9600
                    // Delete
9601
                    $delete = '';
9602
                    if ($repo->isGranted('DELETE', $exerciseEntity) && $allowToEditBaseCourse) {
9603
                        if (!$locked) {
9604
                            $deleteUrl = 'exercise.php?'.api_get_cidreq().'&action=delete&sec_token='.$token.'&exerciseId='.$exerciseId;
9605
                            $delete = Display::url(
9606
                                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
9607
                                '',
9608
                                [
9609
                                    'onclick' => "javascript:if(!confirm('".
9610
                                        addslashes(api_htmlentities(get_lang('Are you sure you want to delete')))." ".
9611
                                        addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9612
                                    'href' => $deleteUrl,
9613
                                ]
9614
                            );
9615
                        } else {
9616
                            $delete = Display::getMdiIcon(
9617
                                ActionIcon::DELETE,
9618
                                'ch-tool-icon-disabled',
9619
                                null,
9620
                                ICON_SIZE_SMALL,
9621
                                get_lang(
9622
                                    '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.'
9623
                                )
9624
                            );
9625
                        }
9626
                    }
9627
9628
                    if ($limitTeacherAccess && !api_is_platform_admin()) {
9629
                        $delete = '';
9630
                    }
9631
9632
                    if (!empty($minCategoriesInExercise)) {
9633
                        $cats = TestCategory::getListOfCategoriesForTest($exercise);
9634
                        if (!(count($cats) >= $minCategoriesInExercise)) {
9635
                            continue;
9636
                        }
9637
                    }
9638
                    $actions .= $delete;
9639
9640
                    // Number of questions.
9641
                    $random = $exerciseEntity->getRandom();
9642
                    if ($random > 0 || -1 == $random) {
9643
                        // if random == -1 means use random questions with all questions
9644
                        $random_number_of_question = $random;
9645
                        if (-1 == $random_number_of_question) {
9646
                            $random_number_of_question = $rowi;
9647
                        }
9648
                        if ($exerciseEntity->getRandomByCategory() > 0) {
9649
                            $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9650
                                $exerciseId,
9651
                                $random_number_of_question
9652
                            );
9653
                            $number_of_questions = $nbQuestionsTotal.' ';
9654
                            $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('questions') : get_lang(
9655
                                'Question lower case'
9656
                            );
9657
                            $number_of_questions .= ' - ';
9658
                            $number_of_questions .= min(
9659
                                    TestCategory::getNumberMaxQuestionByCat($exerciseId),
9660
                                    $random_number_of_question
9661
                                ).' '.get_lang('Question by category');
9662
                        } else {
9663
                            $random_label = ' ('.get_lang('Random').') ';
9664
                            $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9665
                            // Bug if we set a random value bigger than the real number of questions
9666
                            if ($random_number_of_question > $rowi) {
9667
                                $number_of_questions = $rowi.' '.$random_label;
9668
                            }
9669
                        }
9670
                    } else {
9671
                        $number_of_questions = $rowi;
9672
                    }
9673
9674
                    $currentRow['count_questions'] = $number_of_questions;
9675
                } else {
9676
                    // Student only.
9677
                    $visibility = $exerciseEntity->isVisible($course, null);
9678
                    if (false === $visibility && !empty($sessionId)) {
9679
                        $visibility = $exerciseEntity->isVisible($course, $session);
9680
                    }
9681
9682
                    if (false === $visibility) {
9683
                        continue;
9684
                    }
9685
9686
                    $url = '<a '.$alt_title.'
9687
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'">'.
9688
                        $cut_title.'</a>';
9689
9690
                    // Link of the exercise.
9691
                    $currentRow['title'] = $url.' '.$sessionStar;
9692
                    // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9693
                    if ($returnData) {
9694
                        $currentRow['title'] = $exercise->getUnformattedTitle();
9695
                    }
9696
9697
                    $sessionCondition = api_get_session_condition(api_get_session_id());
9698
                    // Don't remove this marker: note-query-exe-results
9699
                    $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9700
                            WHERE
9701
                                exe_exo_id = ".$exerciseId." AND
9702
                                exe_user_id = $userId AND
9703
                                c_id = ".api_get_course_int_id()." AND
9704
                                status <> 'incomplete' AND
9705
                                orig_lp_id = 0 AND
9706
                                orig_lp_item_id = 0
9707
                                $sessionCondition
9708
                            ORDER BY exe_id DESC";
9709
9710
                    $qryres = Database::query($sql);
9711
                    $num = Database:: num_rows($qryres);
9712
9713
                    // Hide the results.
9714
                    $my_result_disabled = $exerciseEntity->getResultsDisabled();
9715
                    $attempt_text = '-';
9716
                    // Time limits are on
9717
                    if ($time_limits) {
9718
                        // Exam is ready to be taken
9719
                        if ($is_actived_time) {
9720
                            // Show results
9721
                            if (
9722
                            in_array(
9723
                                $my_result_disabled,
9724
                                [
9725
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9726
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9727
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9728
                                    RESULT_DISABLE_RANKING,
9729
                                ]
9730
                            )
9731
                            ) {
9732
                                // More than one attempt
9733
                                if ($num > 0) {
9734
                                    $row_track = Database:: fetch_array($qryres);
9735
                                    $attempt_text = get_lang('Latest attempt').' : ';
9736
                                    $attempt_text .= ExerciseLib::show_score(
9737
                                        $row_track['score'],
9738
                                        $row_track['max_score']
9739
                                    );
9740
                                } else {
9741
                                    //No attempts
9742
                                    $attempt_text = get_lang('Not attempted');
9743
                                }
9744
                            } else {
9745
                                $attempt_text = '-';
9746
                            }
9747
                        } else {
9748
                            // Quiz not ready due to time limits
9749
                            //@todo use the is_visible function
9750
                            if (!empty($startTime) && !empty($endTime)) {
9751
                                $today = time();
9752
                                if ($today < $start_time) {
9753
                                    $attempt_text = sprintf(
9754
                                        get_lang('Exercise will be activated from %s to %s'),
9755
                                        api_convert_and_format_date($start_time),
9756
                                        api_convert_and_format_date($end_time)
9757
                                    );
9758
                                } else {
9759
                                    if ($today > $end_time) {
9760
                                        $attempt_text = sprintf(
9761
                                            get_lang('Exercise was activated from %s to %s'),
9762
                                            api_convert_and_format_date($start_time),
9763
                                            api_convert_and_format_date($end_time)
9764
                                        );
9765
                                    }
9766
                                }
9767
                            } else {
9768
                                if (!empty($startTime)) {
9769
                                    $attempt_text = sprintf(
9770
                                        get_lang('Exercise available from %s'),
9771
                                        api_convert_and_format_date($start_time)
9772
                                    );
9773
                                }
9774
                                if (!empty($endTime)) {
9775
                                    $attempt_text = sprintf(
9776
                                        get_lang('Exercise available until %s'),
9777
                                        api_convert_and_format_date($end_time)
9778
                                    );
9779
                                }
9780
                            }
9781
                        }
9782
                    } else {
9783
                        // Normal behaviour.
9784
                        // Show results.
9785
                        if (
9786
                        in_array(
9787
                            $my_result_disabled,
9788
                            [
9789
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9790
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9791
                                RESULT_DISABLE_SHOW_SCORE_ONLY,
9792
                                RESULT_DISABLE_RANKING,
9793
                                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
9794
                            ]
9795
                        )
9796
                        ) {
9797
                            if ($num > 0) {
9798
                                $row_track = Database::fetch_array($qryres);
9799
                                $attempt_text = get_lang('Latest attempt').' : ';
9800
                                $attempt_text .= ExerciseLib::show_score(
9801
                                    $row_track['score'],
9802
                                    $row_track['max_score']
9803
                                );
9804
                            } else {
9805
                                $attempt_text = get_lang('Not attempted');
9806
                            }
9807
                        }
9808
                    }
9809
                    if ($returnData) {
9810
                        $attempt_text = $num;
9811
                    }
9812
                }
9813
9814
                $currentRow['attempt'] = $attempt_text;
9815
                $currentRow['iid'] = $exerciseId;
9816
9817
                if ($is_allowedToEdit) {
9818
                    $additionalActions = ExerciseLib::getAdditionalTeacherActions($exerciseId);
9819
9820
                    if (!empty($additionalActions)) {
9821
                        $actions .= $additionalActions.PHP_EOL;
9822
                    }
9823
9824
                    if (!empty($myActions) && is_callable($myActions)) {
9825
                        $actions = $myActions($currentRow);
9826
                    }
9827
                    $currentRow = [
9828
                        $exerciseId,
9829
                        $currentRow['title'],
9830
                        $currentRow['count_questions'],
9831
                        $actions,
9832
                    ];
9833
                } else {
9834
                    $currentRow = [
9835
                        $currentRow['title'],
9836
                        $currentRow['attempt'],
9837
                    ];
9838
9839
                    if ($isDrhOfCourse) {
9840
                        $currentRow[] = '<a
9841
                            href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9842
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).
9843
                            '</a>';
9844
                    }
9845
                    if ($returnData) {
9846
                        $currentRow['id'] = $exercise->id;
9847
                        $currentRow['url'] = $webPath.'exercise/overview.php?'
9848
                            .api_get_cidreq().'&'
9849
                            ."$mylpid$mylpitemid&exerciseId={$exercise->id}";
9850
                        $currentRow['name'] = $currentRow[0];
9851
                    }
9852
                }
9853
                $tableRows[] = $currentRow;
9854
            }
9855
        }
9856
9857
        if (empty($tableRows) && empty($categoryId)) {
9858
            if ($is_allowedToEdit && 'learnpath' !== $origin) {
9859
                if (!empty($_GET['keyword'])) {
9860
                    $content .= Display::return_message(
9861
                        sprintf(get_lang('No result for keyword %s'),Security::remove_XSS($_GET['keyword'])),
9862
                        'warning'
9863
                    );
9864
                } else {
9865
                    $content .= Display::noDataView(
9866
                        get_lang('Test'),
9867
                        Display::getMdiIcon(ToolIcon::QUIZ, 'ch-tool-icon', null, ICON_SIZE_BIG),
9868
                        get_lang('Create a new test'),
9869
                        'exercise_admin.php?'.api_get_cidreq()
9870
                    );
9871
                }
9872
            }
9873
        } else {
9874
            if (empty($tableRows)) {
9875
                return '';
9876
            }
9877
            $table->setTableData($tableRows);
9878
            $table->setTotalNumberOfItems($total);
9879
            $table->set_additional_parameters(
9880
                [
9881
                    'cid' => api_get_course_int_id(),
9882
                    'sid' => api_get_session_id(),
9883
                    'category_id' => $categoryId,
9884
                ]
9885
            );
9886
9887
            if ($is_allowedToEdit) {
9888
                $formActions = [];
9889
                $formActions['visible'] = get_lang('Activate');
9890
                $formActions['invisible'] = get_lang('Deactivate');
9891
                $formActions['delete'] = get_lang('Delete');
9892
                $table->set_form_actions($formActions);
9893
            }
9894
9895
            $i = 0;
9896
            if ($is_allowedToEdit) {
9897
                $table->set_header($i++, '', false, 'width="18px"');
9898
            }
9899
            $table->set_header($i++, get_lang('Test name'), false);
9900
9901
            if ($is_allowedToEdit) {
9902
                $table->set_header($i++, get_lang('Questions'), false, [], ['class' => 'text-center']);
9903
                $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9904
            } else {
9905
                $table->set_header($i++, get_lang('Status'), false);
9906
                if ($isDrhOfCourse) {
9907
                    $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9908
                }
9909
            }
9910
9911
            if ($returnTable) {
9912
                return $table;
9913
            }
9914
            $content .= $table->return_table();
9915
        }
9916
9917
        return $content;
9918
    }
9919
9920
    /**
9921
     * @return int value in minutes
9922
     */
9923
    public function getResultAccess()
9924
    {
9925
        $extraFieldValue = new ExtraFieldValue('exercise');
9926
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9927
            $this->iId,
9928
            'results_available_for_x_minutes'
9929
        );
9930
9931
        if (!empty($value) && isset($value['value'])) {
9932
            return (int) $value['value'];
9933
        }
9934
9935
        return 0;
9936
    }
9937
9938
    /**
9939
     * @param array $exerciseResultInfo
9940
     *
9941
     * @return bool
9942
     */
9943
    public function getResultAccessTimeDiff($exerciseResultInfo)
9944
    {
9945
        $value = $this->getResultAccess();
9946
        if (!empty($value)) {
9947
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
9948
            $endDate->add(new DateInterval('PT'.$value.'M'));
9949
            $now = time();
9950
            if ($endDate->getTimestamp() > $now) {
9951
                return (int) $endDate->getTimestamp() - $now;
9952
            }
9953
        }
9954
9955
        return 0;
9956
    }
9957
9958
    /**
9959
     * @param array $exerciseResultInfo
9960
     *
9961
     * @return bool
9962
     */
9963
    public function hasResultsAccess($exerciseResultInfo)
9964
    {
9965
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
9966
        if (0 === $diff) {
9967
            return false;
9968
        }
9969
9970
        return true;
9971
    }
9972
9973
    /**
9974
     * @return int
9975
     */
9976
    public function getResultsAccess()
9977
    {
9978
        $extraFieldValue = new ExtraFieldValue('exercise');
9979
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9980
            $this->iId,
9981
            'results_available_for_x_minutes'
9982
        );
9983
        if (!empty($value)) {
9984
            return (int) $value;
9985
        }
9986
9987
        return 0;
9988
    }
9989
9990
    /**
9991
     * @param int   $questionId
9992
     * @param bool  $show_results
9993
     * @param array $question_result
9994
     */
9995
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
9996
    {
9997
        $id = (int) $objQuestionTmp->id;
9998
        $questionId = (int) $questionId;
9999
10000
        $final_overlap = $question_result['extra']['final_overlap'];
10001
        $final_missing = $question_result['extra']['final_missing'];
10002
        $final_excess = $question_result['extra']['final_excess'];
10003
10004
        $overlap_color = $question_result['extra']['overlap_color'];
10005
        $missing_color = $question_result['extra']['missing_color'];
10006
        $excess_color = $question_result['extra']['excess_color'];
10007
10008
        $threadhold1 = $question_result['extra']['threadhold1'];
10009
        $threadhold2 = $question_result['extra']['threadhold2'];
10010
        $threadhold3 = $question_result['extra']['threadhold3'];
10011
10012
        if ($show_results) {
10013
            if ($overlap_color) {
10014
                $overlap_color = 'green';
10015
            } else {
10016
                $overlap_color = 'red';
10017
            }
10018
10019
            if ($missing_color) {
10020
                $missing_color = 'green';
10021
            } else {
10022
                $missing_color = 'red';
10023
            }
10024
            if ($excess_color) {
10025
                $excess_color = 'green';
10026
            } else {
10027
                $excess_color = 'red';
10028
            }
10029
10030
            if (!is_numeric($final_overlap)) {
10031
                $final_overlap = 0;
10032
            }
10033
10034
            if (!is_numeric($final_missing)) {
10035
                $final_missing = 0;
10036
            }
10037
            if (!is_numeric($final_excess)) {
10038
                $final_excess = 0;
10039
            }
10040
10041
            if ($final_excess > 100) {
10042
                $final_excess = 100;
10043
            }
10044
10045
            $table_resume = '
10046
                    <table class="table table-hover table-striped data_table">
10047
                        <tr class="row_odd" >
10048
                            <td>&nbsp;</td>
10049
                            <td><b>'.get_lang('Requirements').'</b></td>
10050
                            <td><b>'.get_lang('Your answer').'</b></td>
10051
                        </tr>
10052
                        <tr class="row_even">
10053
                            <td><b>'.get_lang('Overlapping area').'</b></td>
10054
                            <td>'.get_lang('Minimum overlap').' '.$threadhold1.'</td>
10055
                            <td>
10056
                                <div style="color:'.$overlap_color.'">
10057
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
10058
                                </div>
10059
                            </td>
10060
                        </tr>
10061
                        <tr>
10062
                            <td><b>'.get_lang('Excessive area').'</b></td>
10063
                            <td>'.get_lang('Maximum excess').' '.$threadhold2.'</td>
10064
                            <td>
10065
                                <div style="color:'.$excess_color.'">
10066
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
10067
                                </div>
10068
                            </td>
10069
                        </tr>
10070
                        <tr class="row_even">
10071
                            <td><b>'.get_lang('Missing area').'</b></td>
10072
                            <td>'.get_lang('Maximum missing').' '.$threadhold3.'</td>
10073
                            <td>
10074
                                <div style="color:'.$missing_color.'">
10075
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
10076
                                </div>
10077
                            </td>
10078
                        </tr>
10079
                    </table>
10080
                ';
10081
10082
            $answerType = $objQuestionTmp->selectType();
10083
            /*if ($next == 0) {
10084
                $try = $try_hotspot;
10085
                $lp = $lp_hotspot;
10086
                $destinationid = $select_question_hotspot;
10087
                $url = $url_hotspot;
10088
            } else {
10089
                //show if no error
10090
                $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
10091
                $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
10092
            }
10093
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
10094
            if ($organs_at_risk_hit > 0) {
10095
                $message = '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
10096
                $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
10097
            } else {
10098
                $message = '<p>'.get_lang('Your delineation :').'</p>';
10099
                $message .= $table_resume;
10100
                $message .= '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
10101
            }
10102
            $message .= '<p>'.$comment.'</p>';
10103
            echo $message;*/
10104
10105
            // Showing the score
10106
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
10107
                          WHERE exe_id = $id AND question_id =  $questionId";
10108
            $resfree = Database::query($queryfree);
10109
            $questionScore = Database::result($resfree, 0, 'marks');
10110
            $totalScore += $questionScore;*/
10111
            $relPath = api_get_path(REL_CODE_PATH);
10112
            echo '</table></td></tr>';
10113
            echo "
10114
                        <tr>
10115
                            <td colspan=\"2\">
10116
                                <div id=\"hotspot-solution\"></div>
10117
                                <script>
10118
                                    $(function() {
10119
                                        new HotspotQuestion({
10120
                                            questionId: $questionId,
10121
                                            exerciseId: {$this->id},
10122
                                            exeId: $id,
10123
                                            selector: '#hotspot-solution',
10124
                                            for: 'solution',
10125
                                            relPath: '$relPath'
10126
                                        });
10127
                                    });
10128
                                </script>
10129
                            </td>
10130
                        </tr>
10131
                    </table>
10132
                ";
10133
        }
10134
    }
10135
10136
    /**
10137
     * Clean exercise session variables.
10138
     */
10139
    public static function cleanSessionVariables()
10140
    {
10141
        Session::erase('objExercise');
10142
        Session::erase('exe_id');
10143
        Session::erase('calculatedAnswerId');
10144
        Session::erase('duration_time_previous');
10145
        Session::erase('duration_time');
10146
        Session::erase('objQuestion');
10147
        Session::erase('objAnswer');
10148
        Session::erase('questionList');
10149
        Session::erase('categoryList');
10150
        Session::erase('exerciseResult');
10151
        Session::erase('firstTime');
10152
10153
        Session::erase('time_per_question');
10154
        Session::erase('question_start');
10155
        Session::erase('exerciseResultCoordinates');
10156
        Session::erase('hotspot_coord');
10157
        Session::erase('hotspot_dest');
10158
        Session::erase('hotspot_delineation_result');
10159
    }
10160
10161
    /**
10162
     * Get the first LP found matching the session ID.
10163
     *
10164
     * @param int $sessionId
10165
     *
10166
     * @return array
10167
     */
10168
    public function getLpBySession($sessionId)
10169
    {
10170
        if (!empty($this->lpList)) {
10171
            $sessionId = (int) $sessionId;
10172
10173
            foreach ($this->lpList as $lp) {
10174
                if (isset($lp['session_id']) && (int) $lp['session_id'] == $sessionId) {
10175
                    return $lp;
10176
                }
10177
            }
10178
10179
            return current($this->lpList);
10180
        }
10181
10182
        return [
10183
            'lp_id' => 0,
10184
            'max_score' => 0,
10185
            'session_id' => 0,
10186
        ];
10187
    }
10188
10189
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id, $course_id = null)
10190
    {
10191
        $lp = Session::read('oLP');
10192
10193
        $safe_exe_id = (int) $safe_exe_id;
10194
        $safe_item_id = (int) $safe_item_id;
10195
10196
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
10197
            return false;
10198
        }
10199
10200
        $viewId = $lp->get_view_id();
10201
        if (!isset($course_id)) {
10202
            $course_id = api_get_course_int_id();
10203
        }
10204
        $userId = (int) api_get_user_id();
10205
        $viewId = (int) $viewId;
10206
10207
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
10208
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
10209
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
10210
10211
        $sql = "SELECT start_date, exe_date, score, max_score, exe_exo_id, exe_duration
10212
                FROM $TBL_TRACK_EXERCICES
10213
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
10214
        $res = Database::query($sql);
10215
        $row_dates = Database::fetch_array($res);
10216
10217
        if (empty($row_dates)) {
10218
            return false;
10219
        }
10220
10221
        $duration = (int) $row_dates['exe_duration'];
10222
        $score = (float) $row_dates['score'];
10223
        $max_score = (float) $row_dates['max_score'];
10224
10225
        $sql = "UPDATE $TBL_LP_ITEM SET
10226
                    max_score = '$max_score'
10227
                WHERE iid = $safe_item_id";
10228
        Database::query($sql);
10229
10230
        $sql = "SELECT iid FROM $TBL_LP_ITEM_VIEW
10231
                WHERE
10232
                    lp_item_id = $safe_item_id AND
10233
                    lp_view_id = $viewId
10234
                ORDER BY iid DESC
10235
                LIMIT 1";
10236
        $res_last_attempt = Database::query($sql);
10237
10238
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
10239
            $row_last_attempt = Database::fetch_row($res_last_attempt);
10240
            $lp_item_view_id = $row_last_attempt[0];
10241
10242
            $exercise = new Exercise($course_id);
10243
            $exercise->read($row_dates['exe_exo_id']);
10244
            $status = 'completed';
10245
10246
            if (!empty($exercise->pass_percentage)) {
10247
                $status = 'failed';
10248
                $success = ExerciseLib::isSuccessExerciseResult(
10249
                    $score,
10250
                    $max_score,
10251
                    $exercise->pass_percentage
10252
                );
10253
                if ($success) {
10254
                    $status = 'passed';
10255
                }
10256
            }
10257
10258
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
10259
                        status = '$status',
10260
                        score = '$score',
10261
                        total_time = '$duration'
10262
                    WHERE iid = $lp_item_view_id";
10263
            Database::query($sql);
10264
10265
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
10266
                        orig_lp_item_view_id = '$lp_item_view_id'
10267
                    WHERE exe_id = ".$safe_exe_id;
10268
            Database::query($sql);
10269
        }
10270
    }
10271
10272
    /**
10273
     * Get the user answers saved in exercise.
10274
     *
10275
     * @param int $attemptId
10276
     *
10277
     * @return array
10278
     */
10279
    public function getUserAnswersSavedInExercise($attemptId)
10280
    {
10281
        $exerciseResult = [];
10282
10283
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
10284
10285
        foreach ($attemptList as $questionId => $options) {
10286
            foreach ($options as $option) {
10287
                $question = Question::read($option['question_id']);
10288
10289
                if ($question) {
10290
                    switch ($question->type) {
10291
                        case FILL_IN_BLANKS:
10292
                        case FILL_IN_BLANKS_COMBINATION:
10293
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
10294
                            if ($option['answer'] === "0") {
10295
                                $option['answer'] = "there is 0 as answer so we do not want to consider it empty";
10296
                            }
10297
                            break;
10298
                    }
10299
                }
10300
10301
                if (!empty($option['answer'])) {
10302
                    $exerciseResult[] = $questionId;
10303
10304
                    break;
10305
                }
10306
            }
10307
        }
10308
10309
        return $exerciseResult;
10310
    }
10311
10312
    /**
10313
     * Get the number of user answers saved in exercise.
10314
     *
10315
     * @param int $attemptId
10316
     *
10317
     * @return int
10318
     */
10319
    public function countUserAnswersSavedInExercise($attemptId)
10320
    {
10321
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
10322
10323
        return count($answers);
10324
    }
10325
10326
    public static function allowAction($action)
10327
    {
10328
        if (api_is_platform_admin()) {
10329
            return true;
10330
        }
10331
10332
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
10333
        $disableClean = ('true' === api_get_setting('exercise.disable_clean_exercise_results_for_teachers'));
10334
10335
        switch ($action) {
10336
            case 'delete':
10337
                if (api_is_allowed_to_edit(null, true)) {
10338
                    if ($limitTeacherAccess) {
10339
                        return false;
10340
                    }
10341
10342
                    return true;
10343
                }
10344
                break;
10345
            case 'clean_results':
10346
                if (api_is_allowed_to_edit(null, true)) {
10347
                    if ($limitTeacherAccess) {
10348
                        return false;
10349
                    }
10350
10351
                    if ($disableClean) {
10352
                        return false;
10353
                    }
10354
10355
                    return true;
10356
                }
10357
10358
                break;
10359
        }
10360
10361
        return false;
10362
    }
10363
10364
    public static function getLpListFromExercise($exerciseId, $courseId)
10365
    {
10366
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
10367
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
10368
10369
        $exerciseId = (int) $exerciseId;
10370
        $courseId = (int) $courseId;
10371
10372
        $sql = "SELECT
10373
                    lp.title,
10374
                    lpi.lp_id,
10375
                    lpi.max_score
10376
                FROM $tableLpItem lpi
10377
                INNER JOIN $tblLp lp
10378
                ON (lpi.lp_id = lp.iid)
10379
                WHERE
10380
                    lpi.item_type = '".TOOL_QUIZ."' AND
10381
                    lpi.path = '$exerciseId'";
10382
        $result = Database::query($sql);
10383
        $lpList = [];
10384
        if (Database::num_rows($result) > 0) {
10385
            $lpList = Database::store_result($result, 'ASSOC');
10386
        }
10387
10388
        return $lpList;
10389
    }
10390
10391
    public function getReminderTable($questionList, $exercise_stat_info, $disableCheckBoxes = false)
10392
    {
10393
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10394
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10395
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10396
        $categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
10397
10398
        if (empty($exercise_stat_info)) {
10399
            return '';
10400
        }
10401
10402
        $remindList = $exercise_stat_info['questions_to_check'];
10403
        $remindList = explode(',', $remindList);
10404
10405
        $exeId = $exercise_stat_info['exe_id'];
10406
        $exerciseId = $exercise_stat_info['exe_exo_id'];
10407
        $exercise_result = $this->getUserAnswersSavedInExercise($exeId);
10408
10409
        $content = Display::label(get_lang('Questions without answer'), 'danger');
10410
        $content .= '<div class="clear"></div><br />';
10411
        $table = '';
10412
        $counter = 0;
10413
        // Loop over all question to show results for each of them, one by one
10414
        foreach ($questionList as $questionId) {
10415
            $objQuestionTmp = Question::read($questionId);
10416
            $check_id = 'remind_list['.$questionId.']';
10417
            $attributes = [
10418
                'id' => $check_id,
10419
                'onclick' => "save_remind_item(this, '$questionId');",
10420
                'data-question-id' => $questionId,
10421
            ];
10422
            if (in_array($questionId, $remindList)) {
10423
                $attributes['checked'] = 1;
10424
            }
10425
10426
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '', $attributes);
10427
            $checkbox = '<div class="pretty p-svg p-curve">
10428
                        '.$checkbox.'
10429
                        <div class="state p-primary ">
10430
                         <svg class="svg svg-icon" viewBox="0 0 20 20">
10431
                            <path d="M7.629,14.566c0.125,0.125,0.291,0.188,0.456,0.188c0.164,0,0.329-0.062,0.456-0.188l8.219-8.221c0.252-0.252,0.252-0.659,0-0.911c-0.252-0.252-0.659-0.252-0.911,0l-7.764,7.763L4.152,9.267c-0.252-0.251-0.66-0.251-0.911,0c-0.252,0.252-0.252,0.66,0,0.911L7.629,14.566z" style="stroke: white;fill:white;"></path>
10432
                         </svg>
10433
                         <label>&nbsp;</label>
10434
                        </div>
10435
                    </div>';
10436
            $counter++;
10437
            $questionTitle = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
10438
            // Check if the question doesn't have an answer.
10439
            if (!in_array($questionId, $exercise_result)) {
10440
                $questionTitle = Display::label($questionTitle, 'danger');
10441
            }
10442
10443
            $label_attributes = [];
10444
            $label_attributes['for'] = $check_id;
10445
            if (false === $disableCheckBoxes) {
10446
                $questionTitle = Display::tag('label', $checkbox.$questionTitle, $label_attributes);
10447
            }
10448
            $table .= Display::div($questionTitle, ['class' => 'exercise_reminder_item ']);
10449
        }
10450
10451
        $content .= Display::div('', ['id' => 'message']).
10452
            Display::div($table, ['class' => 'question-check-test']);
10453
10454
        $content .= '<script>
10455
        var lp_data = $.param({
10456
            "learnpath_id": '.$learnpath_id.',
10457
            "learnpath_item_id" : '.$learnpath_item_id.',
10458
            "learnpath_item_view_id": '.$learnpath_item_view_id.'
10459
        });
10460
10461
        function final_submit() {
10462
            // Normal inputs.
10463
            window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
10464
        }
10465
10466
        function selectAll() {
10467
            $("input[type=checkbox]").each(function () {
10468
                $(this).prop("checked", 1);
10469
                var question_id = $(this).data("question-id");
10470
                var action = "add";
10471
                $.ajax({
10472
                    url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10473
                    data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10474
                    success: function(returnValue) {
10475
                    }
10476
                });
10477
            });
10478
        }
10479
10480
        function changeOptionStatus(status)
10481
        {
10482
            $("input[type=checkbox]").each(function () {
10483
                $(this).prop("checked", status);
10484
            });
10485
10486
            var action = "";
10487
            var option = "remove_all";
10488
            if (status == 1) {
10489
                option = "add_all";
10490
            }
10491
            $.ajax({
10492
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10493
                data: "option="+option+"&exe_id='.$exeId.'&action="+action,
10494
                success: function(returnValue) {
10495
                }
10496
            });
10497
        }
10498
10499
        function reviewQuestions() {
10500
            var isChecked = 1;
10501
            $("input[type=checkbox]").each(function () {
10502
                if ($(this).prop("checked")) {
10503
                    isChecked = 2;
10504
                    return false;
10505
                }
10506
            });
10507
10508
            if (isChecked == 1) {
10509
                $("#message").addClass("warning-message");
10510
                $("#message").html("'.addslashes(get_lang('Select a question to revise')).'");
10511
            } else {
10512
                window.location = "exercise_submit.php?'.api_get_cidreq().'&category_id='.$categoryId.'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
10513
            }
10514
        }
10515
10516
        function save_remind_item(obj, question_id) {
10517
            var action = "";
10518
            if ($(obj).prop("checked")) {
10519
                action = "add";
10520
            } else {
10521
                action = "delete";
10522
            }
10523
            $.ajax({
10524
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10525
                data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10526
                success: function(returnValue) {
10527
                }
10528
            });
10529
        }
10530
        </script>';
10531
10532
        return $content;
10533
    }
10534
10535
    public function getRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10536
    {
10537
        $dataSet = [];
10538
        $labels = [];
10539
        $labelsWithId = [];
10540
        /** @var Exercise $exercise */
10541
        foreach ($exercises as $exercise) {
10542
            if (empty($labels)) {
10543
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10544
                if (!empty($categoryNameList)) {
10545
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10546
                    asort($labelsWithId);
10547
                    $labels = array_values($labelsWithId);
10548
                }
10549
            }
10550
10551
            foreach ($userList as $userId) {
10552
                $results = Event::getExerciseResultsByUser(
10553
                    $userId,
10554
                    $exercise->iId,
10555
                    $courseId,
10556
                    $sessionId
10557
                );
10558
10559
                if ($results) {
10560
                    $firstAttempt = end($results);
10561
                    $exeId = $firstAttempt['exe_id'];
10562
10563
                    ob_start();
10564
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10565
                        $exercise,
10566
                        $exeId,
10567
                        false
10568
                    );
10569
                    ob_end_clean();
10570
10571
                    $categoryList = $stats['category_list'];
10572
                    $tempResult = [];
10573
                    foreach ($labelsWithId as $category_id => $title) {
10574
                        if (isset($categoryList[$category_id])) {
10575
                            $category_item = $categoryList[$category_id];
10576
                            $tempResult[] = round($category_item['score'] / $category_item['total'] * 10);
10577
                        } else {
10578
                            $tempResult[] = 0;
10579
                        }
10580
                    }
10581
                    $dataSet[] = $tempResult;
10582
                }
10583
            }
10584
        }
10585
10586
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10587
    }
10588
10589
    public function getAverageRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10590
    {
10591
        $dataSet = [];
10592
        $labels = [];
10593
        $labelsWithId = [];
10594
10595
        $tempResult = [];
10596
        /** @var Exercise $exercise */
10597
        foreach ($exercises as $exercise) {
10598
            $exerciseId = $exercise->iId;
10599
            if (empty($labels)) {
10600
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10601
                if (!empty($categoryNameList)) {
10602
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10603
                    asort($labelsWithId);
10604
                    $labels = array_values($labelsWithId);
10605
                }
10606
            }
10607
10608
            foreach ($userList as $userId) {
10609
                $results = Event::getExerciseResultsByUser(
10610
                    $userId,
10611
                    $exerciseId,
10612
                    $courseId,
10613
                    $sessionId
10614
                );
10615
10616
                if ($results) {
10617
                    $firstAttempt = end($results);
10618
                    $exeId = $firstAttempt['exe_id'];
10619
10620
                    ob_start();
10621
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10622
                        $exercise,
10623
                        $exeId,
10624
                        false
10625
                    );
10626
                    ob_end_clean();
10627
10628
                    $categoryList = $stats['category_list'];
10629
                    foreach ($labelsWithId as $category_id => $title) {
10630
                        if (isset($categoryList[$category_id])) {
10631
                            $category_item = $categoryList[$category_id];
10632
                            if (!isset($tempResult[$exerciseId][$category_id])) {
10633
                                $tempResult[$exerciseId][$category_id] = 0;
10634
                            }
10635
                            $tempResult[$exerciseId][$category_id] += $category_item['score'] / $category_item['total'] * 10;
10636
                        }
10637
                    }
10638
                }
10639
            }
10640
        }
10641
10642
        $totalUsers = count($userList);
10643
10644
        foreach ($exercises as $exercise) {
10645
            $exerciseId = $exercise->iId;
10646
            $data = [];
10647
            foreach ($labelsWithId as $category_id => $title) {
10648
                if (isset($tempResult[$exerciseId]) && isset($tempResult[$exerciseId][$category_id])) {
10649
                    $data[] = round($tempResult[$exerciseId][$category_id] / $totalUsers);
10650
                } else {
10651
                    $data[] = 0;
10652
                }
10653
            }
10654
            $dataSet[] = $data;
10655
        }
10656
10657
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10658
    }
10659
10660
    public function getRadar($labels, $dataSet, $dataSetLabels = [])
10661
    {
10662
        if (empty($labels) || empty($dataSet)) {
10663
            return '';
10664
        }
10665
10666
        $displayLegend = 0;
10667
        if (!empty($dataSetLabels)) {
10668
            $displayLegend = 1;
10669
        }
10670
10671
        $labels = json_encode($labels);
10672
10673
        $colorList = ChamiloHelper::getColorPalette(true, true);
10674
10675
        $dataSetToJson = [];
10676
        $counter = 0;
10677
        foreach ($dataSet as $index => $resultsArray) {
10678
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0, 255).', '.rand(0, 255).', '.rand(0, 255).', 1.0)';
10679
10680
            $label = isset($dataSetLabels[$index]) ? $dataSetLabels[$index] : '';
10681
            $background = str_replace('1.0', '0.2', $color);
10682
            $dataSetToJson[] = [
10683
                'fill' => false,
10684
                'label' => $label,
10685
                'backgroundColor' => $background,
10686
                'borderColor' => $color,
10687
                'pointBackgroundColor' => $color,
10688
                'pointBorderColor' => '#fff',
10689
                'pointHoverBackgroundColor' => '#fff',
10690
                'pointHoverBorderColor' => $color,
10691
                'pointRadius' => 6,
10692
                'pointBorderWidth' => 3,
10693
                'pointHoverRadius' => 10,
10694
                'data' => $resultsArray,
10695
            ];
10696
            $counter++;
10697
        }
10698
        $resultsToJson = json_encode($dataSetToJson);
10699
10700
        return "
10701
                <canvas id='categoryRadar' height='200'></canvas>
10702
                <script>
10703
                    var data = {
10704
                        labels: $labels,
10705
                        datasets: $resultsToJson
10706
                    }
10707
                    var options = {
10708
                        responsive: true,
10709
                        scale: {
10710
                            angleLines: {
10711
                                display: false
10712
                            },
10713
                            ticks: {
10714
                                beginAtZero: true,
10715
                                  min: 0,
10716
                                  max: 10,
10717
                                stepSize: 1,
10718
                            },
10719
                            pointLabels: {
10720
                              fontSize: 14,
10721
                              //fontStyle: 'bold'
10722
                            },
10723
                        },
10724
                        elements: {
10725
                            line: {
10726
                                tension: 0,
10727
                                borderWidth: 3
10728
                            }
10729
                        },
10730
                        legend: {
10731
                            //position: 'bottom'
10732
                            display: $displayLegend
10733
                        },
10734
                        animation: {
10735
                            animateScale: true,
10736
                            animateRotate: true
10737
                        },
10738
                    };
10739
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
10740
                    var myRadarChart = new Chart(ctx, {
10741
                        type: 'radar',
10742
                        data: data,
10743
                        options: options
10744
                    });
10745
                </script>
10746
                ";
10747
    }
10748
10749
    /**
10750
     * Returns true if the exercise is locked by percentage. an exercise attempt must be passed.
10751
     */
10752
    public function isBlockedByPercentage(array $attempt = []): bool
10753
    {
10754
        if (empty($attempt)) {
10755
            return false;
10756
        }
10757
        $extraFieldValue = new ExtraFieldValue('exercise');
10758
        $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
10759
            $this->iId,
10760
            'blocking_percentage'
10761
        );
10762
10763
        if (empty($blockExercise['value'])) {
10764
            return false;
10765
        }
10766
10767
        $blockPercentage = (int) $blockExercise['value'];
10768
10769
        if (0 === $blockPercentage) {
10770
            return false;
10771
        }
10772
10773
        $resultPercentage = 0;
10774
10775
        if (isset($attempt['score']) && isset($attempt['max_score'])) {
10776
            $weight = (int) $attempt['max_score'];
10777
            $weight = (0 == $weight) ? 1 : $weight;
10778
            $resultPercentage = float_format(
10779
                ($attempt['score'] / $weight) * 100,
10780
                1
10781
            );
10782
        }
10783
        if ($resultPercentage <= $blockPercentage) {
10784
            return true;
10785
        }
10786
10787
        return false;
10788
    }
10789
10790
    /**
10791
     * Gets the question list ordered by the question_order setting (drag and drop).
10792
     *
10793
     * @param bool $adminView Optional.
10794
     *
10795
     * @return array
10796
     */
10797
    public function getQuestionOrderedList($adminView = false)
10798
    {
10799
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
10800
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
10801
10802
        // Getting question_order to verify that the question
10803
        // list is correct and all question_order's were set
10804
        $sql = "SELECT DISTINCT count(e.question_order) as count
10805
                FROM $TBL_EXERCICE_QUESTION e
10806
                INNER JOIN $TBL_QUESTIONS q
10807
                ON (e.question_id = q.iid)
10808
                WHERE
10809
                  e.quiz_id	= ".$this->getId();
10810
10811
        $result = Database::query($sql);
10812
        $row = Database::fetch_array($result);
10813
        $count_question_orders = $row['count'];
10814
10815
        // Getting question list from the order (question list drag n drop interface).
10816
        $sql = "SELECT DISTINCT e.question_id, e.question_order
10817
                FROM $TBL_EXERCICE_QUESTION e
10818
                INNER JOIN $TBL_QUESTIONS q
10819
                ON (e.question_id = q.iid)
10820
                WHERE
10821
10822
                    e.quiz_id = '".$this->getId()."'
10823
                ORDER BY question_order";
10824
        $result = Database::query($sql);
10825
10826
        // Fills the array with the question ID for this exercise
10827
        // the key of the array is the question position
10828
        $temp_question_list = [];
10829
        $counter = 1;
10830
        $questionList = [];
10831
        while ($new_object = Database::fetch_object($result)) {
10832
            if (!$adminView) {
10833
                // Correct order.
10834
                $questionList[$new_object->question_order] = $new_object->question_id;
10835
            } else {
10836
                $questionList[$counter] = $new_object->question_id;
10837
            }
10838
10839
            // Just in case we save the order in other array
10840
            $temp_question_list[$counter] = $new_object->question_id;
10841
            $counter++;
10842
        }
10843
10844
        if (!empty($temp_question_list)) {
10845
            /* If both array don't match it means that question_order was not correctly set
10846
               for all questions using the default mysql order */
10847
            if (count($temp_question_list) != $count_question_orders) {
10848
                $questionList = $temp_question_list;
10849
            }
10850
        }
10851
10852
        return $questionList;
10853
    }
10854
10855
    /**
10856
     * Get number of questions in exercise by user attempt.
10857
     *
10858
     * @return int
10859
     */
10860
    private function countQuestionsInExercise()
10861
    {
10862
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10863
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10864
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10865
10866
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
10867
10868
        if (!empty($trackInfo)) {
10869
            $questionIds = explode(',', $trackInfo['data_tracking']);
10870
10871
            return count($questionIds);
10872
        }
10873
10874
        return $this->getQuestionCount();
10875
    }
10876
10877
    /**
10878
     * Select N values from the questions per category array.
10879
     *
10880
     * @param array $categoriesAddedInExercise
10881
     * @param array $question_list
10882
     * @param array $questions_by_category
10883
     * @param bool  $flatResult
10884
     * @param bool  $randomizeQuestions
10885
     * @param array $questionsByCategoryMandatory
10886
     *
10887
     * @return array
10888
     */
10889
    private function pickQuestionsPerCategory(
10890
        $categoriesAddedInExercise,
10891
        $question_list,
10892
        &$questions_by_category,
10893
        $flatResult = true,
10894
        $randomizeQuestions = false,
10895
        $questionsByCategoryMandatory = []
10896
    ) {
10897
        $addAll = true;
10898
        $categoryCountArray = [];
10899
10900
        // Getting how many questions will be selected per category.
10901
        if (!empty($categoriesAddedInExercise)) {
10902
            $addAll = false;
10903
            // Parsing question according the category rel exercise settings
10904
            foreach ($categoriesAddedInExercise as $category_info) {
10905
                $category_id = $category_info['category_id'];
10906
                if (isset($questions_by_category[$category_id])) {
10907
                    // How many question will be picked from this category.
10908
                    $count = $category_info['count_questions'];
10909
                    // -1 means all questions
10910
                    $categoryCountArray[$category_id] = $count;
10911
                    if (-1 == $count) {
10912
                        $categoryCountArray[$category_id] = 999;
10913
                    }
10914
                }
10915
            }
10916
        }
10917
10918
        if (!empty($questions_by_category)) {
10919
            $temp_question_list = [];
10920
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
10921
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
10922
                    $numberOfQuestions = 0;
10923
                    if (isset($categoryCountArray[$category_id])) {
10924
                        $numberOfQuestions = $categoryCountArray[$category_id];
10925
                    }
10926
                }
10927
10928
                if ($addAll) {
10929
                    $numberOfQuestions = 999;
10930
                }
10931
                if (!empty($numberOfQuestions)) {
10932
                    $mandatoryQuestions = [];
10933
                    if (isset($questionsByCategoryMandatory[$category_id])) {
10934
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
10935
                    }
10936
10937
                    $elements = TestCategory::getNElementsFromArray(
10938
                        $categoryQuestionList,
10939
                        $numberOfQuestions,
10940
                        $randomizeQuestions,
10941
                        $mandatoryQuestions
10942
                    );
10943
10944
                    if (!empty($elements)) {
10945
                        $temp_question_list[$category_id] = $elements;
10946
                        $categoryQuestionList = $elements;
10947
                    }
10948
                }
10949
            }
10950
10951
            if (!empty($temp_question_list)) {
10952
                if ($flatResult) {
10953
                    $temp_question_list = array_flatten($temp_question_list);
10954
                }
10955
                $question_list = $temp_question_list;
10956
            }
10957
        }
10958
10959
        return $question_list;
10960
    }
10961
10962
    /**
10963
     * Sends a notification when a user ends an examn.
10964
     *
10965
     * @param array  $question_list_answers
10966
     * @param string $origin
10967
     * @param array  $user_info
10968
     * @param string $url_email
10969
     * @param array  $teachers
10970
     */
10971
    private function sendNotificationForOpenQuestions(
10972
        $question_list_answers,
10973
        $origin,
10974
        $user_info,
10975
        $url_email,
10976
        $teachers
10977
    ) {
10978
        // Email configuration settings
10979
        $courseCode = api_get_course_id();
10980
        $courseInfo = api_get_course_info($courseCode);
10981
        $sessionId = api_get_session_id();
10982
        $sessionData = '';
10983
        if (!empty($sessionId)) {
10984
            $sessionInfo = api_get_session_info($sessionId);
10985
            if (!empty($sessionInfo)) {
10986
                $sessionData = '<tr>'
10987
                    .'<td><em>'.get_lang('Session name').'</em></td>'
10988
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
10989
                    .'</tr>';
10990
            }
10991
        }
10992
10993
        $msg = get_lang('A learner has answered an open question').'<br /><br />'
10994
            .get_lang('Attempt details').' : <br /><br />'
10995
            .'<table>'
10996
            .'<tr>'
10997
            .'<td><em>'.get_lang('Course name').'</em></td>'
10998
            .'<td>&nbsp;<b>#course#</b></td>'
10999
            .'</tr>'
11000
            .$sessionData
11001
            .'<tr>'
11002
            .'<td>'.get_lang('Test attempted').'</td>'
11003
            .'<td>&nbsp;#exercise#</td>'
11004
            .'</tr>'
11005
            .'<tr>'
11006
            .'<td>'.get_lang('Learner name').'</td>'
11007
            .'<td>&nbsp;#firstName# #lastName#</td>'
11008
            .'</tr>'
11009
            .'<tr>'
11010
            .'<td>'.get_lang('Learner e-mail').'</td>'
11011
            .'<td>&nbsp;#mail#</td>'
11012
            .'</tr>'
11013
            .'</table>';
11014
11015
        $open_question_list = null;
11016
        foreach ($question_list_answers as $item) {
11017
            $question = $item['question'];
11018
            $answer = $item['answer'];
11019
            $answer_type = $item['answer_type'];
11020
11021
            if (!empty($question) && !empty($answer) && FREE_ANSWER == $answer_type) {
11022
                $open_question_list .=
11023
                    '<tr>
11024
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
11025
                    <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
11026
                    </tr>
11027
                    <tr>
11028
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
11029
                    <td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>
11030
                    </tr>';
11031
            }
11032
        }
11033
11034
        if (!empty($open_question_list)) {
11035
            $msg .= '<p><br />'.get_lang('A learner has answered an open question').' :</p>'.
11036
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
11037
            $msg .= $open_question_list;
11038
            $msg .= '</table><br />';
11039
11040
            $msg = str_replace('#exercise#', $this->exercise, $msg);
11041
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
11042
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
11043
            $msg = str_replace('#mail#', $user_info['email'], $msg);
11044
            $msg = str_replace(
11045
                '#course#',
11046
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?sid='.$sessionId),
11047
                $msg
11048
            );
11049
11050
            if ('learnpath' !== $origin) {
11051
                $msg .= '<br /><a href="#url#">'.get_lang(
11052
                        'Click this link to check the answer and/or give feedback'
11053
                    ).'</a>';
11054
            }
11055
            $msg = str_replace('#url#', $url_email, $msg);
11056
            $subject = get_lang('A learner has answered an open question');
11057
11058
            if (!empty($teachers)) {
11059
                foreach ($teachers as $user_id => $teacher_data) {
11060
                    MessageManager::send_message_simple(
11061
                        $user_id,
11062
                        $subject,
11063
                        $msg
11064
                    );
11065
                }
11066
            }
11067
        }
11068
    }
11069
11070
    /**
11071
     * Send notification for oral questions.
11072
     *
11073
     * @param array  $question_list_answers
11074
     * @param string $origin
11075
     * @param int    $exe_id
11076
     * @param array  $user_info
11077
     * @param string $url_email
11078
     * @param array  $teachers
11079
     */
11080
    private function sendNotificationForOralQuestions(
11081
        $question_list_answers,
11082
        $origin,
11083
        $exe_id,
11084
        $user_info,
11085
        $url_email,
11086
        $teachers
11087
    ): void {
11088
11089
        // Email configuration settings
11090
        $courseCode = api_get_course_id();
11091
        $courseInfo = api_get_course_info($courseCode);
11092
        $oral_question_list = null;
11093
        foreach ($question_list_answers as $item) {
11094
            $question = $item['question'];
11095
            $file = $item['generated_oral_file'];
11096
            $answer = $item['answer'];
11097
            if (0 == $answer) {
11098
                $answer = '';
11099
            }
11100
            $answer_type = $item['answer_type'];
11101
            if (!empty($question) && (!empty($answer) || !empty($file)) && ORAL_EXPRESSION == $answer_type) {
11102
                $oral_question_list .= '<br />
11103
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
11104
                    <tr>
11105
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
11106
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
11107
                    </tr>
11108
                    <tr>
11109
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
11110
                        <td valign="top" bgcolor="#F3F3F3"><p>'.$answer.'</p><p>'.$file.'</p></td>
11111
                    </tr></table>';
11112
            }
11113
        }
11114
11115
        if (!empty($oral_question_list)) {
11116
            $msg = get_lang('A learner has attempted one or more oral question').'<br /><br />
11117
                    '.get_lang('Attempt details').' : <br /><br />
11118
                    <table>
11119
                        <tr>
11120
                            <td><em>'.get_lang('Course name').'</em></td>
11121
                            <td>&nbsp;<b>#course#</b></td>
11122
                        </tr>
11123
                        <tr>
11124
                            <td>'.get_lang('Test attempted').'</td>
11125
                            <td>&nbsp;#exercise#</td>
11126
                        </tr>
11127
                        <tr>
11128
                            <td>'.get_lang('Learner name').'</td>
11129
                            <td>&nbsp;#firstName# #lastName#</td>
11130
                        </tr>
11131
                        <tr>
11132
                            <td>'.get_lang('Learner e-mail').'</td>
11133
                            <td>&nbsp;#mail#</td>
11134
                        </tr>
11135
                    </table>';
11136
            $msg .= '<br />'.sprintf(
11137
                    get_lang('The attempted oral questions are %s'),
11138
                    $oral_question_list
11139
                ).'<br />';
11140
            $msg1 = str_replace('#exercise#', $this->exercise, $msg);
11141
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg1);
11142
            $msg1 = str_replace('#lastName#', $user_info['lastname'], $msg);
11143
            $msg = str_replace('#mail#', $user_info['email'], $msg1);
11144
            $msg1 = str_replace('#course#', $courseInfo['name'], $msg);
11145
11146
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
11147
                $msg1 .= '<br /><a href="#url#">'.get_lang(
11148
                        'Click this link to check the answer and/or give feedback'
11149
                    ).'</a>';
11150
            }
11151
            $msg = str_replace('#url#', $url_email, $msg1);
11152
            $mail_content = $msg;
11153
            $subject = get_lang('A learner has attempted one or more oral question');
11154
11155
            if (!empty($teachers)) {
11156
                foreach ($teachers as $user_id => $teacher_data) {
11157
                    MessageManager::send_message_simple(
11158
                        $user_id,
11159
                        $subject,
11160
                        $mail_content
11161
                    );
11162
                }
11163
            }
11164
        }
11165
    }
11166
11167
    /**
11168
     * Returns an array with the media list.
11169
     *
11170
     * @param array $questionList question list
11171
     *
11172
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
11173
     * <code>
11174
     * array (size=2)
11175
     *  999 =>
11176
     *    array (size=3)
11177
     *      0 => int 7
11178
     *      1 => int 6
11179
     *      2 => int 3254
11180
     *  100 =>
11181
     *   array (size=1)
11182
     *      0 => int 5
11183
     *  </code>
11184
     */
11185
    private function setMediaList($questionList)
11186
    {
11187
        $mediaList = [];
11188
        /*
11189
         * Media feature is not activated in 1.11.x
11190
        if (!empty($questionList)) {
11191
            foreach ($questionList as $questionId) {
11192
                $objQuestionTmp = Question::read($questionId, $this->course_id);
11193
                // If a media question exists
11194
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
11195
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
11196
                } else {
11197
                    // Always the last item
11198
                    $mediaList[999][] = $objQuestionTmp->id;
11199
                }
11200
            }
11201
        }*/
11202
11203
        $this->mediaList = $mediaList;
11204
    }
11205
11206
    /**
11207
     * @return HTML_QuickForm_group
11208
     */
11209
    private function setResultDisabledGroup(FormValidator $form)
11210
    {
11211
        $resultDisabledGroup = [];
11212
11213
        $resultDisabledGroup[] = $form->createElement(
11214
            'radio',
11215
            'results_disabled',
11216
            null,
11217
            get_lang('Auto-evaluation mode: show score and expected answers'),
11218
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
11219
            ['id' => 'result_disabled_0']
11220
        );
11221
11222
        $warning = sprintf(
11223
            get_lang("The setting \"%s\" will change to \"%s\""),
11224
            get_lang('Feedback'),
11225
            get_lang('Exam (no feedback)')
11226
        );
11227
        $resultDisabledGroup[] = $form->createElement(
11228
            'radio',
11229
            'results_disabled',
11230
            null,
11231
            get_lang('Exam mode: Do not show score nor answers'),
11232
            RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS,
11233
            [
11234
                'id' => 'result_disabled_1',
11235
                //'onclick' => 'check_results_disabled()'
11236
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11237
            ]
11238
        );
11239
11240
        $resultDisabledGroup[] = $form->createElement(
11241
            'radio',
11242
            'results_disabled',
11243
            null,
11244
            get_lang('Practice mode: Show score only, by category if at least one is used'),
11245
            RESULT_DISABLE_SHOW_SCORE_ONLY,
11246
            [
11247
                'id' => 'result_disabled_2',
11248
                //'onclick' => 'check_results_disabled()'
11249
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11250
            ]
11251
        );
11252
11253
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
11254
            return $form->addGroup(
11255
                $resultDisabledGroup,
11256
                null,
11257
                get_lang(
11258
                    'Show score to learner'
11259
                )
11260
            );
11261
        }
11262
11263
        $resultDisabledGroup[] = $form->createElement(
11264
            'radio',
11265
            'results_disabled',
11266
            null,
11267
            get_lang(
11268
                'Show score on every attempt, show correct answers only on last attempt (only works with an attempts limit)'
11269
            ),
11270
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
11271
            ['id' => 'result_disabled_4']
11272
        );
11273
11274
        $resultDisabledGroup[] = $form->createElement(
11275
            'radio',
11276
            'results_disabled',
11277
            null,
11278
            get_lang(
11279
                'Do not show the score (only when user finishes all attempts) but show feedback for each attempt.'
11280
            ),
11281
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
11282
            [
11283
                'id' => 'result_disabled_5',
11284
                //'onclick' => 'check_results_disabled()'
11285
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11286
            ]
11287
        );
11288
11289
        $resultDisabledGroup[] = $form->createElement(
11290
            'radio',
11291
            'results_disabled',
11292
            null,
11293
            get_lang(
11294
                'Ranking mode: Do not show results details question by question and show a table with the ranking of all other users.'
11295
            ),
11296
            RESULT_DISABLE_RANKING,
11297
            ['id' => 'result_disabled_6']
11298
        );
11299
11300
        $resultDisabledGroup[] = $form->createElement(
11301
            'radio',
11302
            'results_disabled',
11303
            null,
11304
            get_lang(
11305
                'Show only global score (not question score) and show only the correct answers, do not show incorrect answers at all'
11306
            ),
11307
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
11308
            ['id' => 'result_disabled_7']
11309
        );
11310
11311
        $resultDisabledGroup[] = $form->createElement(
11312
            'radio',
11313
            'results_disabled',
11314
            null,
11315
            get_lang('Auto-evaluation mode and ranking'),
11316
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
11317
            ['id' => 'result_disabled_8']
11318
        );
11319
11320
        $resultDisabledGroup[] = $form->createElement(
11321
            'radio',
11322
            'results_disabled',
11323
            null,
11324
            get_lang('Show score by category on a radar/spiderweb chart'),
11325
            RESULT_DISABLE_RADAR,
11326
            ['id' => 'result_disabled_9']
11327
        );
11328
11329
        $resultDisabledGroup[] = $form->createElement(
11330
            'radio',
11331
            'results_disabled',
11332
            null,
11333
            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.'),
11334
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
11335
            ['id' => 'result_disabled_10']
11336
        );
11337
11338
        return $form->addGroup(
11339
            $resultDisabledGroup,
11340
            null,
11341
            get_lang('Show score to learner')
11342
        );
11343
    }
11344
11345
    /**
11346
     * Return the text to display, based on the score and the max score.
11347
     * @param int|float $score
11348
     * @param int|float $maxScore
11349
     * @return string
11350
     */
11351
    public function getFinishText(int|float $score, int|float $maxScore): string
11352
    {
11353
        $passPercentage = $this->selectPassPercentage();
11354
        if (!empty($passPercentage)) {
11355
            $percentage = float_format(
11356
                ($score / (0 != $maxScore ? $maxScore : 1)) * 100,
11357
                1
11358
            );
11359
            if ($percentage >= $passPercentage) {
11360
                return $this->getTextWhenFinished();
11361
            } else {
11362
                return $this->getTextWhenFinishedFailure();
11363
            }
11364
        } else {
11365
            return $this->getTextWhenFinished();
11366
        }
11367
11368
        return '';
11369
    }
11370
}
11371