Passed
Push — master ( 6289d1...16e4a5 )
by
unknown
19:17 queued 09:02
created

Exercise   F

Complexity

Total Complexity 1515

Size/Duplication

Total Lines 11388
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 6216
dl 0
loc 11388
rs 0.8
c 0
b 0
f 0
wmc 1515

187 Methods

Rating   Name   Duplication   Size   Complexity  
A getCutTitle() 0 5 1
A updateAttempts() 0 3 1
A setRandom() 0 3 1
A updateCategories() 0 5 2
A updateResultsDisabled() 0 3 1
A enable_results() 0 3 1
A updateTitle() 0 3 1
A updateDescription() 0 3 1
A enable() 0 3 1
A updateType() 0 3 1
A updatePassPercentage() 0 3 1
A updateExpiredTime() 0 3 1
A updateRandomAnswers() 0 3 1
A disable() 0 3 1
A setModelType() 0 3 1
B updateSound() 0 29 8
A updateEndButton() 0 3 1
A setNotifyUserByEmail() 0 3 1
A getQuestionSelectionType() 0 3 1
A updateEmailNotificationTemplate() 0 3 1
A updatePropagateNegative() 0 3 1
A updateFeedbackType() 0 3 1
A setEmailNotificationTemplateToUser() 0 3 1
A setOnSuccessMessage() 0 3 1
A setOnFailedMessage() 0 3 1
A disable_results() 0 3 1
A updateSaveCorrectAnswers() 0 3 1
A setQuestionSelectionType() 0 3 1
A updateReviewAnswers() 0 3 3
A getId() 0 3 1
A isInList() 0 8 2
A addToList() 0 18 4
A removeFromList() 0 20 6
A update_question_positions() 0 24 4
B selectQuestionList() 0 38 7
A getRandomByCategory() 0 3 1
A getSound() 0 3 1
A getHideQuestionTitle() 0 3 1
A hasQuestionWithTypeNotInList() 0 22 2
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 mediaIsActivated() 0 18 6
A saveCategoriesInExercise() 0 15 5
A getQuestionForTeacher() 0 28 3
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
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 3027 514
B get_validated_question_list() 0 62 8
B progressExercisePaginationBar() 0 58 7
B get_exercise_result() 0 37 6
A get_stat_track_exercise_info_by_exe_id() 0 19 5
A setNotifications() 0 3 1
A getQuizCategoryId() 0 7 2
A getAutoLaunch() 0 3 1
A getQuestionWithCategories() 0 24 2
A setHideQuestionNumber() 0 3 1
A getCategoriesInExercise() 0 18 4
A get_question_list() 0 6 1
D generateStats() 0 135 19
C editQuestionToRemind() 0 52 13
A setPageResultConfiguration() 0 12 2
A getHideQuestionNumber() 0 3 1
F is_visible() 0 243 44
C getNextQuestionId() 0 70 16
A getQuestionListWithMediasCompressed() 0 3 1
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 removeAllQuestionToRemind() 0 13 2
A getUnformattedTitle() 0 3 1
A getQuestionList() 0 3 1
A getPageConfigurationAttribute() 0 9 2
A get_formated_title_variable() 0 3 1
C transform_question_list_with_medias() 0 42 12
B getMaxScore() 0 41 9
A showExpectedChoiceColumn() 0 24 5
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
A returnTimeLeftDiv() 0 20 1
C getDelineationResult() 0 131 12
A getResultsAccess() 0 12 2
A countUserAnswersSavedInExercise() 0 5 1
B getUserAnswersSavedInExercise() 0 31 8
A getResultAccess() 0 13 3
B allowAction() 0 36 9
F exerciseGridResource() 0 772 122
A cleanSessionVariables() 0 20 1
A getResultAccessTimeDiff() 0 13 3
A getLpListFromExercise() 0 25 2
B saveExerciseInLp() 0 80 10
A hasResultsAccess() 0 8 2
A getLpBySession() 0 18 5
A countQuestionsInExercise() 0 15 5
C pickQuestionsPerCategory() 0 71 16
A getUserAnswersSavedInExerciseStatic() 0 8 3
B sendNotificationForOralQuestions() 0 81 11
A getFinishText() 0 18 4
B getReminderTable() 0 133 10
A getQuestionOrderedList() 0 56 5
A setMediaList() 0 19 1
B isBlockedByPercentage() 0 36 8
B getRadarsFromUsers() 0 52 8
C sendNotificationForOpenQuestions() 0 93 11
C getAverageRadarsFromUsers() 0 69 13
B setResultDisabledGroup() 0 133 2
B getRadar() 0 73 7

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
            UPLOAD_ANSWER == $answerType
3658
        ) {
3659
            $nbrAnswers = 1;
3660
        }
3661
3662
        $generatedFilesHtml = '';
3663
        if ($answerType == ORAL_EXPRESSION) {
3664
            $generatedFilesHtml = ExerciseLib::getOralFileAudio($exeId, $questionId);
3665
        }
3666
3667
        $user_answer = '';
3668
        // Get answer list for matching
3669
        $sql = "SELECT iid, answer
3670
                FROM $table_ans
3671
                WHERE question_id = $questionId";
3672
        $res_answer = Database::query($sql);
3673
3674
        $answerMatching = [];
3675
        while ($real_answer = Database::fetch_array($res_answer)) {
3676
            $answerMatching[$real_answer['iid']] = $real_answer['answer'];
3677
        }
3678
3679
        // Get first answer needed for global question, no matter the answer shuffle option;
3680
        $firstAnswer = [];
3681
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
3682
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
3683
        ) {
3684
            $sql = "SELECT *
3685
                    FROM $table_ans
3686
                    WHERE question_id = $questionId
3687
                    ORDER BY position
3688
                    LIMIT 1";
3689
            $result = Database::query($sql);
3690
            if (Database::num_rows($result)) {
3691
                $firstAnswer = Database::fetch_array($result);
3692
            }
3693
        }
3694
3695
        $real_answers = [];
3696
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
3697
3698
        $organs_at_risk_hit = 0;
3699
        $questionScore = 0;
3700
        $orderedHotSpots = [];
3701
        if (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, ANNOTATION], true)) {
3702
            $orderedHotSpots = $em->getRepository(TrackEHotspot::class)->findBy(
3703
                [
3704
                    'hotspotQuestionId' => $questionId,
3705
                    'course' => $course_id,
3706
                    'hotspotExeId' => $exeId,
3707
                ],
3708
                ['hotspotAnswerId' => 'ASC']
3709
            );
3710
        }
3711
3712
        if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION], true)) {
3713
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION == $answerType) {
3714
                $questionScore = $questionWeighting;
3715
            }
3716
            // Load student's selected choices
3717
            if ($from_database) {
3718
                $studentChoices = Database::store_result(
3719
                    Database::query(
3720
                        "SELECT answer FROM $TBL_TRACK_ATTEMPT WHERE exe_id = $exeId AND question_id = $questionId"
3721
                    ),
3722
                    'ASSOC'
3723
                );
3724
                $studentChoices = array_column($studentChoices, 'answer');
3725
            } else {
3726
                $studentChoices = array_values((array) $choice);
3727
            }
3728
3729
            // Compute the list of correct choice IDs
3730
            $correctChoices = array_keys(array_filter(
3731
                $answerMatching, // iid => answer text
3732
                function ($answerIdKey) use ($objAnswerTmp) {
3733
                    // Safeguards in case arrays are not initialized
3734
                    $iidList = is_array($objAnswerTmp->iid) ? $objAnswerTmp->iid : [];
3735
                    $pos = array_search($answerIdKey, $iidList, true);
3736
                    return $pos !== false && !empty($objAnswerTmp->correct[$pos]);
3737
                },
3738
                ARRAY_FILTER_USE_KEY
3739
            ));
3740
3741
            // Scoring
3742
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION === $answerType) {
3743
                // All-or-nothing scoring
3744
                $questionScore = (float) $questionWeighting;
3745
                if (array_diff($studentChoices, $correctChoices) || array_diff($correctChoices, $studentChoices)) {
3746
                    $questionScore = 0.0;
3747
                }
3748
            } else { // MULTIPLE_ANSWER_DROPDOWN
3749
                // Partial scoring using per-choice weighting
3750
                $questionScore = 0.0;
3751
                $iidList    = is_array($objAnswerTmp->iid) ? $objAnswerTmp->iid : [];
3752
                $correctArr = is_array($objAnswerTmp->correct) ? $objAnswerTmp->correct : [];
3753
                $weights    = is_array($objAnswerTmp->weighting) ? $objAnswerTmp->weighting : [];
3754
3755
                foreach ((array) $studentChoices as $choiceId) {
3756
                    $pos = array_search($choiceId, $iidList, true);
3757
                    if ($pos !== false && !empty($correctArr[$pos])) {
3758
                        $questionScore += (float) ($weights[$pos] ?? 0);
3759
                    }
3760
                }
3761
            }
3762
3763
            // Render (UI)
3764
            if ($show_result) {
3765
                echo ExerciseShowFunctions::displayMultipleAnswerDropdown(
3766
                    $this,
3767
                    $objAnswerTmp,
3768
                    $correctChoices,
3769
                    $studentChoices,
3770
                    $showTotalScoreAndUserChoicesInLastAttempt
3771
                );
3772
            }
3773
        }
3774
3775
        if ($debug) {
3776
            error_log('-- Start answer loop --');
3777
        }
3778
3779
        $answerDestination = null;
3780
        $userAnsweredQuestion = false;
3781
        $correctAnswerId = [];
3782
        $matchingCorrectAnswers = [];
3783
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3784
            $answer = $objAnswerTmp->selectAnswer($answerId);
3785
            $answerComment = $objAnswerTmp->selectComment($answerId);
3786
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3787
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3788
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3789
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3790
3791
            if ($debug) {
3792
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3793
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3794
                error_log("answerWeighting: $answerWeighting");
3795
            }
3796
3797
            // Delineation
3798
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3799
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3800
3801
            switch ($answerType) {
3802
                case UNIQUE_ANSWER:
3803
                case UNIQUE_ANSWER_IMAGE:
3804
                case UNIQUE_ANSWER_NO_OPTION:
3805
                case READING_COMPREHENSION:
3806
                    if ($from_database) {
3807
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3808
                                WHERE
3809
                                    exe_id = $exeId AND
3810
                                    question_id = $questionId";
3811
                        $result = Database::query($sql);
3812
                        $choice = Database::result($result, 0, 'answer');
3813
3814
                        if (false === $userAnsweredQuestion) {
3815
                            $userAnsweredQuestion = !empty($choice);
3816
                        }
3817
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3818
                        if ($studentChoice) {
3819
                            $questionScore += $answerWeighting;
3820
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3821
                            $correctAnswerId[] = $answerId;
3822
                        }
3823
                    } else {
3824
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3825
                        if ($studentChoice) {
3826
                            $questionScore += $answerWeighting;
3827
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3828
                            $correctAnswerId[] = $answerId;
3829
                        }
3830
                    }
3831
3832
                    break;
3833
                case MULTIPLE_ANSWER_TRUE_FALSE:
3834
                    if ($from_database) {
3835
                        $choice = [];
3836
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3837
                                WHERE
3838
                                    exe_id = $exeId AND
3839
                                    question_id = ".$questionId;
3840
3841
                        $result = Database::query($sql);
3842
                        while ($row = Database::fetch_array($result)) {
3843
                            $values = explode(':', $row['answer']);
3844
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3845
                            $option = isset($values[1]) ? $values[1] : '';
3846
                            $choice[$my_answer_id] = $option;
3847
                        }
3848
                        $userAnsweredQuestion = !empty($choice);
3849
                    }
3850
3851
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3852
                    if (!empty($studentChoice)) {
3853
                        $correctAnswerId[] = $answerAutoId;
3854
                        if ($studentChoice == $answerCorrect) {
3855
                            $questionScore += $true_score;
3856
                        } else {
3857
                            if (isset($quiz_question_options[$studentChoice])
3858
                                && in_array($quiz_question_options[$studentChoice]['name'], ["Don't know", 'DoubtScore'])
3859
                            ) {
3860
                                $questionScore += $doubt_score;
3861
                            } else {
3862
                                $questionScore += $false_score;
3863
                            }
3864
                        }
3865
                    } else {
3866
                        // If no result then the user just hit don't know
3867
                        $studentChoice = 3;
3868
                        $questionScore += $doubt_score;
3869
                    }
3870
                    $totalScore = $questionScore;
3871
3872
                    break;
3873
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3874
                    if ($from_database) {
3875
                        $choice = [];
3876
                        $choiceDegreeCertainty = [];
3877
                        $sql = "SELECT answer
3878
                                FROM $TBL_TRACK_ATTEMPT
3879
                                WHERE exe_id = $exeId AND question_id = $questionId";
3880
3881
                        $result = Database::query($sql);
3882
                        while ($row = Database::fetch_array($result)) {
3883
                            $ind = $row['answer'];
3884
                            $values = explode(':', $ind);
3885
                            $myAnswerId = $values[0] ?? null;
3886
                            $option = $values[1] ?? null;
3887
                            $percent = $values[2] ?? null;
3888
                            $choice[$myAnswerId] = $option;
3889
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3890
                        }
3891
                    }
3892
3893
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3894
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3895
3896
                    // student score update
3897
                    if (!empty($studentChoice)) {
3898
                        if ($studentChoice == $answerCorrect) {
3899
                            // correct answer and student is Unsure or PrettySur
3900
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3901
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3902
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3903
                            ) {
3904
                                $questionScore += $true_score;
3905
                            } else {
3906
                                // student ignore correct answer
3907
                                $questionScore += $doubt_score;
3908
                            }
3909
                        } else {
3910
                            // false answer and student is Unsure or PrettySur
3911
                            if (isset($quiz_question_options[$studentChoiceDegree]) && $quiz_question_options[$studentChoiceDegree]['position'] >= 3
3912
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3913
                                $questionScore += $false_score;
3914
                            } else {
3915
                                // student ignore correct answer
3916
                                $questionScore += $doubt_score;
3917
                            }
3918
                        }
3919
                    }
3920
                    $totalScore = $questionScore;
3921
3922
                    break;
3923
                case MULTIPLE_ANSWER:
3924
                case MULTIPLE_ANSWER_DROPDOWN:
3925
                    if ($from_database) {
3926
                        $choice = [];
3927
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3928
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3929
                        $resultans = Database::query($sql);
3930
                        while ($row = Database::fetch_array($resultans)) {
3931
                            $choice[$row['answer']] = 1;
3932
                        }
3933
3934
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3935
                        $real_answers[$answerId] = (bool) $studentChoice;
3936
3937
                        if ($studentChoice) {
3938
                            $questionScore += $answerWeighting;
3939
                        }
3940
                    } else {
3941
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3942
                        $real_answers[$answerId] = (bool) $studentChoice;
3943
3944
                        if (isset($studentChoice)
3945
                            || (MULTIPLE_ANSWER_DROPDOWN == $answerType && in_array($answerAutoId, $choice))
3946
                        ) {
3947
                            $correctAnswerId[] = $answerAutoId;
3948
                            $questionScore += $answerWeighting;
3949
                        }
3950
                    }
3951
                    $totalScore += $answerWeighting;
3952
3953
                    break;
3954
                case GLOBAL_MULTIPLE_ANSWER:
3955
                    if ($from_database) {
3956
                        $choice = [];
3957
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3958
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3959
                        $resultans = Database::query($sql);
3960
                        while ($row = Database::fetch_array($resultans)) {
3961
                            $choice[$row['answer']] = 1;
3962
                        }
3963
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3964
                        $real_answers[$answerId] = (bool) $studentChoice;
3965
                        if ($studentChoice) {
3966
                            $questionScore += $answerWeighting;
3967
                        }
3968
                    } else {
3969
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3970
                        if (isset($studentChoice)) {
3971
                            $questionScore += $answerWeighting;
3972
                        }
3973
                        $real_answers[$answerId] = (bool) $studentChoice;
3974
                    }
3975
                    $totalScore += $answerWeighting;
3976
                    if ($debug) {
3977
                        error_log("studentChoice: $studentChoice");
3978
                    }
3979
3980
                    break;
3981
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3982
                    if ($from_database) {
3983
                        $choice = [];
3984
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3985
                                WHERE exe_id = $exeId AND question_id = $questionId";
3986
                        $resultans = Database::query($sql);
3987
                        while ($row = Database::fetch_array($resultans)) {
3988
                            $result = explode(':', $row['answer']);
3989
                            if (isset($result[0])) {
3990
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3991
                                $option = isset($result[1]) ? $result[1] : '';
3992
                                $choice[$my_answer_id] = $option;
3993
                            }
3994
                        }
3995
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3996
3997
                        $real_answers[$answerId] = false;
3998
                        if ($answerCorrect == $studentChoice) {
3999
                            $real_answers[$answerId] = true;
4000
                        }
4001
                    } else {
4002
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
4003
                        $real_answers[$answerId] = false;
4004
                        if ($answerCorrect == $studentChoice) {
4005
                            $real_answers[$answerId] = true;
4006
                        }
4007
                    }
4008
4009
                    break;
4010
                case MULTIPLE_ANSWER_COMBINATION:
4011
                    if ($from_database) {
4012
                        $choice = [];
4013
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4014
                                WHERE exe_id = $exeId AND question_id = $questionId";
4015
                        $resultans = Database::query($sql);
4016
                        while ($row = Database::fetch_array($resultans)) {
4017
                            $choice[$row['answer']] = 1;
4018
                        }
4019
4020
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4021
                        if (1 == $answerCorrect) {
4022
                            $real_answers[$answerId] = false;
4023
                            if ($studentChoice) {
4024
                                $real_answers[$answerId] = true;
4025
                            }
4026
                        } else {
4027
                            $real_answers[$answerId] = true;
4028
                            if ($studentChoice) {
4029
                                $real_answers[$answerId] = false;
4030
                            }
4031
                        }
4032
                    } else {
4033
                        $studentChoice = $choice[$answerAutoId] ?? null;
4034
                        if (1 == $answerCorrect) {
4035
                            $real_answers[$answerId] = false;
4036
                            if ($studentChoice) {
4037
                                $real_answers[$answerId] = true;
4038
                            }
4039
                        } else {
4040
                            $real_answers[$answerId] = true;
4041
                            if ($studentChoice) {
4042
                                $real_answers[$answerId] = false;
4043
                            }
4044
                        }
4045
                    }
4046
4047
                    break;
4048
                case FILL_IN_BLANKS:
4049
                case FILL_IN_BLANKS_COMBINATION:
4050
                    $str = '';
4051
                    $answerFromDatabase = '';
4052
                    if ($from_database) {
4053
                        $sql = "SELECT answer
4054
                                FROM $TBL_TRACK_ATTEMPT
4055
                                WHERE
4056
                                    exe_id = $exeId AND
4057
                                    question_id= $questionId ";
4058
                        $result = Database::query($sql);
4059
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
4060
                    }
4061
4062
                    // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
4063
                    if (false) {
4064
                        // the question is encoded like this
4065
                        // [A] B [C] D [E] F::10,10,10@1
4066
                        // number 1 before the "@" means that is a switchable fill in blank question
4067
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4068
                        // means that is a normal fill blank question
4069
                        // first we explode the "::"
4070
                        $pre_array = explode('::', $answer);
4071
4072
                        // is switchable fill blank or not
4073
                        $last = count($pre_array) - 1;
4074
                        $is_set_switchable = explode('@', $pre_array[$last]);
4075
                        $switchable_answer_set = false;
4076
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4077
                            $switchable_answer_set = true;
4078
                        }
4079
                        $answer = '';
4080
                        for ($k = 0; $k < $last; $k++) {
4081
                            $answer .= $pre_array[$k];
4082
                        }
4083
                        // splits weightings that are joined with a comma
4084
                        $answerWeighting = explode(',', $is_set_switchable[0]);
4085
                        // we save the answer because it will be modified
4086
                        $temp = $answer;
4087
                        $answer = '';
4088
                        $j = 0;
4089
                        //initialise answer tags
4090
                        $user_tags = $correct_tags = $real_text = [];
4091
                        // the loop will stop at the end of the text
4092
                        while (1) {
4093
                            // quits the loop if there are no more blanks (detect '[')
4094
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4095
                                // adds the end of the text
4096
                                $answer = $temp;
4097
                                $real_text[] = $answer;
4098
4099
                                break; //no more "blanks", quit the loop
4100
                            }
4101
                            // adds the piece of text that is before the blank
4102
                            //and ends with '[' into a general storage array
4103
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4104
                            $answer .= api_substr($temp, 0, $pos + 1);
4105
                            //take the string remaining (after the last "[" we found)
4106
                            $temp = api_substr($temp, $pos + 1);
4107
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4108
                            if (false === ($pos = api_strpos($temp, ']'))) {
4109
                                // adds the end of the text
4110
                                $answer .= $temp;
4111
4112
                                break;
4113
                            }
4114
                            if ($from_database) {
4115
                                $str = $answerFromDatabase;
4116
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4117
                                $str = str_replace('\r\n', '', $str);
4118
4119
                                $choice = $arr[1];
4120
                                if (isset($choice[$j])) {
4121
                                    $tmp = api_strrpos($choice[$j], ' / ');
4122
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4123
                                    $choice[$j] = trim($choice[$j]);
4124
                                    // Needed to let characters ' and " to work as part of an answer
4125
                                    $choice[$j] = stripslashes($choice[$j]);
4126
                                } else {
4127
                                    $choice[$j] = null;
4128
                                }
4129
                            } else {
4130
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4131
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4132
                            }
4133
4134
                            $user_tags[] = $choice[$j];
4135
                            // Put the contents of the [] answer tag into correct_tags[]
4136
                            $correct_tags[] = api_substr($temp, 0, $pos);
4137
                            $j++;
4138
                            $temp = api_substr($temp, $pos + 1);
4139
                        }
4140
                        $answer = '';
4141
                        $real_correct_tags = $correct_tags;
4142
                        $chosen_list = [];
4143
4144
                        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...
4145
                            if (0 == $i) {
4146
                                $answer .= $real_text[0];
4147
                            }
4148
                            if (!$switchable_answer_set) {
4149
                                // Needed to parse ' and " characters
4150
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4151
                                if ($correct_tags[$i] == $user_tags[$i]) {
4152
                                    // gives the related weighting to the student
4153
                                    $questionScore += $answerWeighting[$i];
4154
                                    // increments total score
4155
                                    $totalScore += $answerWeighting[$i];
4156
                                    // adds the word in green at the end of the string
4157
                                    $answer .= $correct_tags[$i];
4158
                                } elseif (!empty($user_tags[$i])) {
4159
                                    // else if the word entered by the student IS NOT the same as
4160
                                    // the one defined by the professor
4161
                                    // adds the word in red at the end of the string, and strikes it
4162
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4163
                                } else {
4164
                                    // adds a tabulation if no word has been typed by the student
4165
                                    $answer .= ''; // remove &nbsp; that causes issue
4166
                                }
4167
                            } else {
4168
                                // switchable fill in the blanks
4169
                                if (in_array($user_tags[$i], $correct_tags)) {
4170
                                    $chosen_list[] = $user_tags[$i];
4171
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4172
                                    // gives the related weighting to the student
4173
                                    $questionScore += $answerWeighting[$i];
4174
                                    // increments total score
4175
                                    $totalScore += $answerWeighting[$i];
4176
                                    // adds the word in green at the end of the string
4177
                                    $answer .= $user_tags[$i];
4178
                                } elseif (!empty($user_tags[$i])) {
4179
                                    // else if the word entered by the student IS NOT the same
4180
                                    // as the one defined by the professor
4181
                                    // adds the word in red at the end of the string, and strikes it
4182
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4183
                                } else {
4184
                                    // adds a tabulation if no word has been typed by the student
4185
                                    $answer .= ''; // remove &nbsp; that causes issue
4186
                                }
4187
                            }
4188
4189
                            // adds the correct word, followed by ] to close the blank
4190
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4191
                            if (isset($real_text[$i + 1])) {
4192
                                $answer .= $real_text[$i + 1];
4193
                            }
4194
                        }
4195
                    } else {
4196
                        // insert the student result in the track_e_attempt table, field answer
4197
                        // $answer is the answer like in the c_quiz_answer table for the question
4198
                        // student data are choice[]
4199
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4200
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4201
                        $answerWeighting = $listCorrectAnswers['weighting'];
4202
                        // user choices is an array $choice
4203
4204
                        // get existing user data in n the BDD
4205
                        if ($from_database) {
4206
                            $listStudentResults = FillBlanks::getAnswerInfo(
4207
                                $answerFromDatabase,
4208
                                true
4209
                            );
4210
                            $choice = $listStudentResults['student_answer'];
4211
                        }
4212
4213
                        // loop other all blanks words
4214
                        if (!$switchableAnswerSet) {
4215
                            // not switchable answer, must be in the same place than teacher order
4216
                            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...
4217
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4218
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4219
4220
                                if ($debug) {
4221
                                    error_log("Student answer: $i");
4222
                                    error_log($studentAnswer);
4223
                                }
4224
4225
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4226
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4227
                                // ENT_QUOTES is used in order to transform ' to &#039;
4228
                                if (!$from_database) {
4229
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4230
                                    if ($debug) {
4231
                                        error_log('Student answer cleaned:');
4232
                                        error_log($studentAnswer);
4233
                                    }
4234
                                }
4235
4236
                                $isAnswerCorrect = 0;
4237
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4238
                                    // gives the related weighting to the student
4239
                                    $questionScore += $answerWeighting[$i];
4240
                                    // increments total score
4241
                                    $totalScore += $answerWeighting[$i];
4242
                                    $isAnswerCorrect = 1;
4243
                                }
4244
                                if ($debug) {
4245
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4246
                                }
4247
4248
                                $studentAnswerToShow = $studentAnswer;
4249
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4250
                                if ($debug) {
4251
                                    error_log("Fill in blank type: $type");
4252
                                }
4253
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4254
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4255
                                    if ('' != $studentAnswer) {
4256
                                        foreach ($listMenu as $item) {
4257
                                            if (sha1($item) == $studentAnswer) {
4258
                                                $studentAnswerToShow = $item;
4259
                                            }
4260
                                        }
4261
                                    }
4262
                                }
4263
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4264
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4265
                            }
4266
                        } else {
4267
                            // switchable answer
4268
                            $listStudentAnswerTemp = $choice;
4269
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4270
4271
                            // for every teacher answer, check if there is a student answer
4272
                            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...
4273
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4274
                                $studentAnswerToShow = $studentAnswer;
4275
4276
                                if (empty($studentAnswer)) {
4277
                                    break;
4278
                                }
4279
4280
                                if ($debug) {
4281
                                    error_log("Student answer: $i");
4282
                                    error_log($studentAnswer);
4283
                                }
4284
4285
                                if (!$from_database) {
4286
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4287
                                    if ($debug) {
4288
                                        error_log("Student answer cleaned:");
4289
                                        error_log($studentAnswer);
4290
                                    }
4291
                                }
4292
4293
                                $found = false;
4294
                                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...
4295
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4296
4297
                                    if (!$found) {
4298
                                        if (FillBlanks::isStudentAnswerGood(
4299
                                            $studentAnswer,
4300
                                            $correctAnswer,
4301
                                            $from_database
4302
                                        )) {
4303
                                            $questionScore += $answerWeighting[$i];
4304
                                            $totalScore += $answerWeighting[$i];
4305
                                            $listTeacherAnswerTemp[$j] = '';
4306
                                            $found = true;
4307
                                        }
4308
                                    }
4309
4310
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4311
                                    if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4312
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4313
                                        if (!empty($studentAnswer)) {
4314
                                            foreach ($listMenu as $key => $item) {
4315
                                                if (sha1($item) === $studentAnswer) {
4316
                                                    $studentAnswerToShow = $item;
4317
                                                    break;
4318
                                                }
4319
                                            }
4320
                                        }
4321
                                    }
4322
                                }
4323
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4324
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4325
                            }
4326
                        }
4327
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4328
                    }
4329
4330
                    break;
4331
                case CALCULATED_ANSWER:
4332
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4333
                    if (!empty($calculatedAnswerList)) {
4334
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4335
                        $preArray = explode('@@', $answer);
4336
                        $last = count($preArray) - 1;
4337
                        $answer = '';
4338
                        for ($k = 0; $k < $last; $k++) {
4339
                            $answer .= $preArray[$k];
4340
                        }
4341
                        $answerWeighting = [$answerWeighting];
4342
                        // we save the answer because it will be modified
4343
                        $temp = $answer;
4344
                        $answer = '';
4345
                        $j = 0;
4346
                        // initialise answer tags
4347
                        $userTags = $correctTags = $realText = [];
4348
                        // the loop will stop at the end of the text
4349
                        while (1) {
4350
                            // quits the loop if there are no more blanks (detect '[')
4351
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4352
                                // adds the end of the text
4353
                                $answer = $temp;
4354
                                $realText[] = $answer;
4355
4356
                                break; //no more "blanks", quit the loop
4357
                            }
4358
                            // adds the piece of text that is before the blank
4359
                            // and ends with '[' into a general storage array
4360
                            $realText[] = api_substr($temp, 0, $pos + 1);
4361
                            $answer .= api_substr($temp, 0, $pos + 1);
4362
                            // take the string remaining (after the last "[" we found)
4363
                            $temp = api_substr($temp, $pos + 1);
4364
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4365
                            if (false === ($pos = api_strpos($temp, ']'))) {
4366
                                // adds the end of the text
4367
                                $answer .= $temp;
4368
4369
                                break;
4370
                            }
4371
4372
                            if ($from_database) {
4373
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4374
                                        WHERE
4375
                                            exe_id = $exeId AND
4376
                                            question_id = $questionId ";
4377
                                $result = Database::query($sql);
4378
                                $str = Database::result($result, 0, 'answer');
4379
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4380
                                $str = str_replace('\r\n', '', $str);
4381
                                $choice = $arr[1];
4382
                                if (isset($choice[$j])) {
4383
                                    $tmp = api_strrpos($choice[$j], ' / ');
4384
                                    if ($tmp) {
4385
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4386
                                    } else {
4387
                                        $tmp = ltrim($tmp, '[');
4388
                                        $tmp = rtrim($tmp, ']');
4389
                                    }
4390
                                    $choice[$j] = trim($choice[$j]);
4391
                                    // Needed to let characters ' and " to work as part of an answer
4392
                                    $choice[$j] = stripslashes($choice[$j]);
4393
                                } else {
4394
                                    $choice[$j] = null;
4395
                                }
4396
                            } else {
4397
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4398
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4399
                            }
4400
                            $userTags[] = $choice[$j];
4401
                            // put the contents of the [] answer tag into correct_tags[]
4402
                            $correctTags[] = api_substr($temp, 0, $pos);
4403
                            $j++;
4404
                            $temp = api_substr($temp, $pos + 1);
4405
                        }
4406
                        $answer = '';
4407
                        $realCorrectTags = $correctTags;
4408
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4409
                        $expectedAnswer = '';
4410
                        $calculatedChoice = '';
4411
4412
                        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...
4413
                            if (0 == $i) {
4414
                                $answer .= $realText[0];
4415
                            }
4416
                            // Needed to parse ' and " characters
4417
                            $userTags[$i] = stripslashes($userTags[$i]);
4418
                            if ($correctTags[$i] == $userTags[$i]) {
4419
                                // gives the related weighting to the student
4420
                                $questionScore += $answerWeighting[$i];
4421
                                // increments total score
4422
                                $totalScore += $answerWeighting[$i];
4423
                                // adds the word in green at the end of the string
4424
                                $answer .= $correctTags[$i];
4425
                                $calculatedChoice = $correctTags[$i];
4426
                            } elseif (!empty($userTags[$i])) {
4427
                                // else if the word entered by the student IS NOT the same as
4428
                                // the one defined by the professor
4429
                                // adds the word in red at the end of the string, and strikes it
4430
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4431
                                $calculatedChoice = $userTags[$i];
4432
                            } else {
4433
                                // adds a tabulation if no word has been typed by the student
4434
                                $answer .= ''; // remove &nbsp; that causes issue
4435
                            }
4436
                            // adds the correct word, followed by ] to close the blank
4437
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4438
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4439
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4440
                                $expectedAnswer = $realCorrectTags[$i];
4441
                            }
4442
                            $answer .= ']';
4443
                            if (isset($realText[$i + 1])) {
4444
                                $answer .= $realText[$i + 1];
4445
                            }
4446
                        }
4447
                    } else {
4448
                        if ($from_database) {
4449
                            $sql = "SELECT *
4450
                                    FROM $TBL_TRACK_ATTEMPT
4451
                                    WHERE
4452
                                        exe_id = $exeId AND
4453
                                        question_id = $questionId ";
4454
                            $result = Database::query($sql);
4455
                            $resultData = Database::fetch_assoc($result);
4456
                            $answer = $resultData['answer'];
4457
                            $questionScore = $resultData['marks'];
4458
                        }
4459
                    }
4460
4461
                    break;
4462
                case UPLOAD_ANSWER:
4463
                case FREE_ANSWER:
4464
                    if ($from_database) {
4465
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4466
                                 WHERE
4467
                                    exe_id = $exeId AND
4468
                                    question_id= ".$questionId;
4469
                        $result = Database::query($sql);
4470
                        $data = Database::fetch_array($result);
4471
                        $choice = '';
4472
                        $questionScore = 0;
4473
                        if ($data) {
4474
                            $choice = $data['answer'];
4475
                            $questionScore = $data['marks'];
4476
                        }
4477
4478
                        $choice = str_replace('\r\n', '', $choice);
4479
                        $choice = stripslashes($choice);
4480
4481
                        if (-1 == $questionScore) {
4482
                            $totalScore += 0;
4483
                        } else {
4484
                            $totalScore += $questionScore;
4485
                        }
4486
                        if ('' == $questionScore) {
4487
                            $questionScore = 0;
4488
                        }
4489
                        $arrques = $questionName;
4490
                        $arrans = $choice;
4491
                    } else {
4492
                        $studentChoice = $choice;
4493
                        if ($studentChoice) {
4494
                            //Fixing negative puntation see #2193
4495
                            $questionScore = 0;
4496
                            $totalScore += 0;
4497
                        }
4498
                    }
4499
4500
                    break;
4501
                case ORAL_EXPRESSION:
4502
                    if ($from_database) {
4503
                        $query = "SELECT answer, marks
4504
                                  FROM $TBL_TRACK_ATTEMPT
4505
                                  WHERE
4506
                                        exe_id = $exeId AND
4507
                                        question_id = $questionId
4508
                                 ";
4509
                        $resq = Database::query($query);
4510
                        $row = Database::fetch_assoc($resq);
4511
                        $choice = '';
4512
                        $questionScore = 0;
4513
4514
                        if (is_array($row)) {
4515
                            $choice = $row['answer'];
4516
                            $choice = str_replace('\r\n', '', $choice);
4517
                            $choice = stripslashes($choice);
4518
                            $questionScore = $row['marks'];
4519
                        }
4520
4521
                        if (-1 == $questionScore) {
4522
                            $totalScore += 0;
4523
                        } else {
4524
                            $totalScore += $questionScore;
4525
                        }
4526
                        $arrques = $questionName;
4527
                        $arrans = $choice;
4528
                    } else {
4529
                        $studentChoice = $choice;
4530
                        if ($studentChoice) {
4531
                            //Fixing negative puntation see #2193
4532
                            $questionScore = 0;
4533
                            $totalScore += 0;
4534
                        }
4535
                    }
4536
4537
                    break;
4538
                case MULTIPLE_ANSWER_DROPDOWN:
4539
                    $totalScore += $answerWeighting;
4540
                    break;
4541
                case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
4542
                    break;
4543
                case MATCHING_COMBINATION:
4544
                case MATCHING_DRAGGABLE_COMBINATION:
4545
                case DRAGGABLE:
4546
                case MATCHING_DRAGGABLE:
4547
                case MATCHING:
4548
                    if ($from_database) {
4549
                        $sql = "SELECT iid, answer
4550
                                FROM $table_ans
4551
                                WHERE
4552
                                    question_id = $questionId AND
4553
                                    correct = 0
4554
                                ";
4555
                        $result = Database::query($sql);
4556
                        // Getting the real answer
4557
                        $real_list = [];
4558
                        while ($realAnswer = Database::fetch_array($result)) {
4559
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4560
                        }
4561
4562
                        $orderBy = ' ORDER BY iid ';
4563
                        if (DRAGGABLE == $answerType) {
4564
                            $orderBy = ' ORDER BY correct ';
4565
                        }
4566
4567
                        $sql = "SELECT iid, answer, correct, ponderation
4568
                                FROM $table_ans
4569
                                WHERE
4570
                                    question_id = $questionId AND
4571
                                    correct <> 0
4572
                                $orderBy";
4573
                        $result = Database::query($sql);
4574
                        $options = [];
4575
                        $correctAnswers = [];
4576
                        while ($row = Database::fetch_assoc($result)) {
4577
                            $options[] = $row;
4578
                            $correctAnswers[$row['correct']] = $row['answer'];
4579
                        }
4580
4581
                        $questionScore = 0;
4582
                        $counterAnswer = 1;
4583
                        foreach ($options as $a_answers) {
4584
                            $i_answer_id = $a_answers['iid']; //3
4585
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4586
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4587
                            $i_answer_id_auto = $a_answers['iid']; // 3 - 4
4588
4589
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4590
                                    WHERE
4591
                                        exe_id = '$exeId' AND
4592
                                        question_id = '$questionId' AND
4593
                                        position = '$i_answer_id_auto'";
4594
                            $result = Database::query($sql);
4595
                            $s_user_answer = 0;
4596
                            if (Database::num_rows($result) > 0) {
4597
                                //  rich - good looking
4598
                                $s_user_answer = Database::result($result, 0, 0);
4599
                            }
4600
                            $i_answerWeighting = $a_answers['ponderation'];
4601
                            $user_answer = '';
4602
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4603
4604
                            if (!empty($s_user_answer)) {
4605
                                if (DRAGGABLE == $answerType) {
4606
                                    if ($s_user_answer == $i_answer_correct_answer) {
4607
                                        $questionScore += $i_answerWeighting;
4608
                                        $totalScore += $i_answerWeighting;
4609
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4610
                                        if ($this->showExpectedChoice() && !empty($i_answer_id_auto)) {
4611
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4612
                                        }
4613
                                        $status = Display::label(get_lang('Correct'), 'success');
4614
                                    } else {
4615
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4616
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4617
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4618
                                            $user_answer = $data['answer'];*/
4619
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4620
                                        }
4621
                                    }
4622
                                } else {
4623
                                    if ($s_user_answer == $i_answer_correct_answer) {
4624
                                        $questionScore += $i_answerWeighting;
4625
                                        $totalScore += $i_answerWeighting;
4626
                                        $status = Display::label(get_lang('Correct'), 'success');
4627
4628
                                        // Try with id
4629
                                        if (isset($real_list[$i_answer_id])) {
4630
                                            $user_answer = Display::span(
4631
                                                $real_list[$i_answer_id],
4632
                                                ['style' => 'color: #008000; font-weight: bold;']
4633
                                            );
4634
                                        }
4635
4636
                                        // Try with $i_answer_id_auto
4637
                                        if (empty($user_answer)) {
4638
                                            if (isset($real_list[$i_answer_id_auto])) {
4639
                                                $user_answer = Display::span(
4640
                                                    $real_list[$i_answer_id_auto],
4641
                                                    ['style' => 'color: #008000; font-weight: bold;']
4642
                                                );
4643
                                            }
4644
                                        }
4645
4646
                                        if (isset($real_list[$i_answer_correct_answer])) {
4647
                                            $matchingCorrectAnswers[$questionId]['from_database']['correct'][$i_answer_correct_answer] = $real_list[$i_answer_correct_answer];
4648
                                            $user_answer = Display::span(
4649
                                                $real_list[$i_answer_correct_answer],
4650
                                                ['style' => 'color: #008000; font-weight: bold;']
4651
                                            );
4652
                                        }
4653
                                    } else {
4654
                                        $user_answer = Display::span(
4655
                                            $real_list[$s_user_answer],
4656
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4657
                                        );
4658
                                        if ($this->showExpectedChoice()) {
4659
                                            if (isset($real_list[$s_user_answer])) {
4660
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4661
                                            }
4662
                                        }
4663
                                    }
4664
                                }
4665
                            } elseif (DRAGGABLE == $answerType) {
4666
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4667
                                if ($this->showExpectedChoice()) {
4668
                                    $user_answer = '';
4669
                                }
4670
                            } else {
4671
                                $user_answer = Display::span(
4672
                                    get_lang('Incorrect').' &nbsp;',
4673
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4674
                                );
4675
                                if ($this->showExpectedChoice()) {
4676
                                    $user_answer = '';
4677
                                }
4678
                            }
4679
4680
                            if ($show_result) {
4681
                                if (false === $this->showExpectedChoice() &&
4682
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4683
                                ) {
4684
                                    $user_answer = '';
4685
                                }
4686
                                switch ($answerType) {
4687
                                    case MATCHING:
4688
                                    case MATCHING_DRAGGABLE:
4689
                                    case MATCHING_COMBINATION:
4690
                                    case MATCHING_DRAGGABLE_COMBINATION:
4691
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4692
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4693
                                                break;
4694
                                            }
4695
                                        }
4696
                                        echo '<tr>';
4697
                                        if (!in_array(
4698
                                            $this->results_disabled,
4699
                                            [
4700
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4701
                                            ]
4702
                                        )
4703
                                        ) {
4704
                                            echo '<td>'.$s_answer_label.'</td>';
4705
                                            echo '<td>'.$user_answer.'</td>';
4706
                                        } else {
4707
                                            echo '<td>'.$s_answer_label.'</td>';
4708
                                            $status = Display::label(get_lang('Correct'), 'success');
4709
                                        }
4710
4711
                                        if ($this->showExpectedChoice()) {
4712
                                            if ($this->showExpectedChoiceColumn()) {
4713
                                                echo '<td>';
4714
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4715
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4716
                                                        true == $showTotalScoreAndUserChoicesInLastAttempt
4717
                                                    ) {
4718
                                                        echo Display::span(
4719
                                                            $real_list[$i_answer_correct_answer]
4720
                                                        );
4721
                                                    }
4722
                                                }
4723
                                                echo '</td>';
4724
                                            }
4725
                                            echo '<td class="text-center">'.$status.'</td>';
4726
                                        } else {
4727
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4728
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4729
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4730
                                                ) {
4731
                                                    if ($this->showExpectedChoiceColumn()) {
4732
                                                        echo '<td>';
4733
                                                        echo Display::span(
4734
                                                            $real_list[$i_answer_correct_answer],
4735
                                                            ['style' => 'color: #008000; font-weight: bold;']
4736
                                                        );
4737
                                                        echo '</td>';
4738
                                                    }
4739
                                                }
4740
                                            }
4741
                                        }
4742
                                        echo '</tr>';
4743
4744
                                        break;
4745
                                    case DRAGGABLE:
4746
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4747
                                            $s_answer_label = '';
4748
                                        }
4749
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4750
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4751
                                                break;
4752
                                            }
4753
                                        }
4754
                                        echo '<tr>';
4755
                                        if ($this->showExpectedChoice()) {
4756
                                            if (!in_array(
4757
                                                $this->results_disabled,
4758
                                                [
4759
                                                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4760
                                                    //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4761
                                                ]
4762
                                            )
4763
                                            ) {
4764
                                                echo '<td>'.$user_answer.'</td>';
4765
                                            } else {
4766
                                                $status = Display::label(get_lang('Correct'), 'success');
4767
                                            }
4768
                                            echo '<td>'.$s_answer_label.'</td>';
4769
                                            echo '<td class="text-center">'.$status.'</td>';
4770
                                        } else {
4771
                                            echo '<td>'.$s_answer_label.'</td>';
4772
                                            echo '<td>'.$user_answer.'</td>';
4773
                                            echo '<td>';
4774
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4775
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4776
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4777
                                                ) {
4778
                                                    echo Display::span(
4779
                                                        $real_list[$i_answer_correct_answer],
4780
                                                        ['style' => 'color: #008000; font-weight: bold;']
4781
                                                    );
4782
                                                }
4783
                                            }
4784
                                            echo '</td>';
4785
                                        }
4786
                                        echo '</tr>';
4787
4788
                                        break;
4789
                                }
4790
                            }
4791
                            $counterAnswer++;
4792
                        }
4793
                        $matchingCorrectAnswers[$questionId]['from_database']['count_options'] = count($options);
4794
                        break 2; // break the switch and the "for" condition
4795
                    } else {
4796
                        if ($answerCorrect) {
4797
                            if (isset($choice[$answerAutoId]) &&
4798
                                $answerCorrect == $choice[$answerAutoId]
4799
                            ) {
4800
                                $matchingCorrectAnswers[$questionId]['form_values']['correct'][$answerAutoId] = $choice[$answerAutoId];
4801
                                $correctAnswerId[] = $answerAutoId;
4802
                                $questionScore += $answerWeighting;
4803
                                $totalScore += $answerWeighting;
4804
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4805
                            } else {
4806
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4807
                                    $user_answer = Display::span(
4808
                                        $answerMatching[$choice[$answerAutoId]],
4809
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4810
                                    );
4811
                                }
4812
                            }
4813
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4814
                        }
4815
                        $matchingCorrectAnswers[$questionId]['form_values']['count_options'] = count($choice);
4816
                    }
4817
4818
                    break;
4819
                case HOT_SPOT:
4820
                case HOT_SPOT_COMBINATION:
4821
                    if ($from_database) {
4822
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4823
                        // Check auto id
4824
                        $foundAnswerId = $answerAutoId;
4825
                        $sql = "SELECT hotspot_correct
4826
                                FROM $TBL_TRACK_HOTSPOT
4827
                                WHERE
4828
                                    hotspot_exe_id = $exeId AND
4829
                                    hotspot_question_id= $questionId AND
4830
                                    hotspot_answer_id = $answerAutoId
4831
                                ORDER BY hotspot_id ASC";
4832
                        $result = Database::query($sql);
4833
                        if (Database::num_rows($result)) {
4834
                            $studentChoice = Database::result(
4835
                                $result,
4836
                                0,
4837
                                'hotspot_correct'
4838
                            );
4839
4840
                            if ($studentChoice) {
4841
                                $questionScore += $answerWeighting;
4842
                                $totalScore += $answerWeighting;
4843
                            }
4844
                        } else {
4845
                            // If answer.id is different:
4846
                            $sql = "SELECT hotspot_correct
4847
                                FROM $TBL_TRACK_HOTSPOT
4848
                                WHERE
4849
                                    hotspot_exe_id = $exeId AND
4850
                                    hotspot_question_id= $questionId AND
4851
                                    hotspot_answer_id = ".(int) $answerId.'
4852
                                ORDER BY hotspot_id ASC';
4853
                            $result = Database::query($sql);
4854
4855
                            $foundAnswerId = $answerId;
4856
                            if (Database::num_rows($result)) {
4857
                                $studentChoice = Database::result(
4858
                                    $result,
4859
                                    0,
4860
                                    'hotspot_correct'
4861
                                );
4862
4863
                                if ($studentChoice) {
4864
                                    $questionScore += $answerWeighting;
4865
                                    $totalScore += $answerWeighting;
4866
                                }
4867
                            } else {
4868
                                // check answer.iid
4869
                                if (!empty($answerIid)) {
4870
                                    $sql = "SELECT hotspot_correct
4871
                                            FROM $TBL_TRACK_HOTSPOT
4872
                                            WHERE
4873
                                                hotspot_exe_id = $exeId AND
4874
                                                hotspot_question_id= $questionId AND
4875
                                                hotspot_answer_id = $answerIid
4876
                                            ORDER BY hotspot_id ASC";
4877
                                    $result = Database::query($sql);
4878
4879
                                    $foundAnswerId = $answerIid;
4880
                                    $studentChoice = Database::result(
4881
                                        $result,
4882
                                        0,
4883
                                        'hotspot_correct'
4884
                                    );
4885
4886
                                    if ($studentChoice) {
4887
                                        $questionScore += $answerWeighting;
4888
                                        $totalScore += $answerWeighting;
4889
                                    }
4890
                                }
4891
                            }
4892
                        }
4893
                    } else {
4894
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4895
                            $choice[$answerAutoId] = 0;
4896
                            $choice[$answerIid] = 0;
4897
                        } else {
4898
                            $studentChoice = $choice[$answerAutoId];
4899
                            if (empty($studentChoice)) {
4900
                                $studentChoice = $choice[$answerIid];
4901
                            }
4902
                            $choiceIsValid = false;
4903
                            if (!empty($studentChoice)) {
4904
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4905
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4906
                                $choicePoint = Geometry::decodePoint($studentChoice);
4907
4908
                                switch ($hotspotType) {
4909
                                    case 'square':
4910
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4911
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4912
4913
                                        break;
4914
                                    case 'circle':
4915
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4916
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4917
4918
                                        break;
4919
                                    case 'poly':
4920
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4921
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4922
4923
                                        break;
4924
                                }
4925
                            }
4926
4927
                            $choice[$answerAutoId] = 0;
4928
                            if ($choiceIsValid) {
4929
                                $questionScore += $answerWeighting;
4930
                                $totalScore += $answerWeighting;
4931
                                $choice[$answerAutoId] = 1;
4932
                                $choice[$answerIid] = 1;
4933
                            }
4934
                            $studentChoice = $choiceIsValid ? 1 : 0;
4935
                        }
4936
                    }
4937
4938
                    break;
4939
                case HOT_SPOT_ORDER:
4940
                    // @todo never added to chamilo
4941
                    // for hotspot with fixed order
4942
                    $studentChoice = $choice['order'][$answerId];
4943
                    if ($studentChoice == $answerId) {
4944
                        $questionScore += $answerWeighting;
4945
                        $totalScore += $answerWeighting;
4946
                        $studentChoice = true;
4947
                    } else {
4948
                        $studentChoice = false;
4949
                    }
4950
4951
                    break;
4952
                case HOT_SPOT_DELINEATION:
4953
                    // for hotspot with delineation
4954
                    if ($from_database) {
4955
                        // getting the user answer
4956
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4957
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4958
                                    FROM $TBL_TRACK_HOTSPOT
4959
                                    WHERE
4960
                                        hotspot_exe_id = $exeId AND
4961
                                        hotspot_question_id= $questionId AND
4962
                                        hotspot_answer_id = '1'";
4963
                        // By default we take 1 because it's a delineation
4964
                        $resq = Database::query($query);
4965
                        $row = Database::fetch_assoc($resq);
4966
4967
                        if ($row && isset($row['hotspot_correct'], $row['hotspot_coordinate'])) {
4968
                            $choice = $row['hotspot_correct'];
4969
                            $user_answer = $row['hotspot_coordinate'];
4970
                            $coords = explode('/', $user_answer);
4971
                        } else {
4972
                            $choice = '';
4973
                            $user_answer = '';
4974
                            $coords = [];
4975
                        }
4976
4977
                        $user_array = '';
4978
                        foreach ($coords as $coord) {
4979
                            [$x, $y] = explode(';', $coord);
4980
                            $user_array .= round($x).';'.round($y).'/';
4981
                        }
4982
                        $user_array = substr($user_array, 0, -1) ?: '';
4983
                    } else {
4984
                        if (!empty($studentChoice)) {
4985
                            $correctAnswerId[] = $answerAutoId;
4986
                            $newquestionList[] = $questionId;
4987
                        }
4988
4989
                        if (1 === $answerId && isset($choice[$answerId])) {
4990
                            $studentChoice = $choice[$answerId];
4991
                            $questionScore += $answerWeighting;
4992
                        }
4993
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4994
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4995
                        }
4996
                    }
4997
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4998
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4999
5000
                    break;
5001
                case FILL_IN_BLANKS_COMBINATION:
5002
                        $answerFromDatabase = '';
5003
                        if ($from_database) {
5004
                            $sql = "SELECT answer
5005
                            FROM $TBL_TRACK_ATTEMPT
5006
                            WHERE exe_id = $exeId AND question_id = $questionId";
5007
                            $result = Database::query($sql);
5008
                            $answerFromDatabase = Database::result($result, 0, 'answer');
5009
                        }
5010
5011
                        // Teacher definition and weights
5012
                        $info = FillBlanks::getAnswerInfo($answer);
5013
                        $listCorrectAnswers = $info; // keep words, weighting, switchable
5014
                        $switchableAnswerSet = $info['switchable'];
5015
                        $answerWeighting = $info['weighting'];
5016
5017
                        // Student choices
5018
                        if ($from_database && $answerFromDatabase !== '') {
5019
                            $studentInfo = FillBlanks::getAnswerInfo($answerFromDatabase, true);
5020
                            $choice = $studentInfo['student_answer'] ?? [];
5021
                        }
5022
5023
                        // Evaluate each blank and fill student_answer/student_score only
5024
                        if (!$switchableAnswerSet) {
5025
                            // Fixed positions
5026
                            $count = count($info['words']);
5027
                            for ($i = 0; $i < $count; $i++) {
5028
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
5029
                                if (!$from_database) {
5030
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
5031
                                }
5032
                                $correctAnswer = $info['words'][$i];
5033
5034
                                // Resolve menu placeholders when present (for display)
5035
                                $studentAnswerToShow = $studentAnswer;
5036
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
5037
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type && $studentAnswer !== '') {
5038
                                    $menu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
5039
                                    foreach ($menu as $opt) {
5040
                                        if (sha1($opt) === $studentAnswer) {
5041
                                            $studentAnswerToShow = $opt;
5042
                                            break;
5043
                                        }
5044
                                    }
5045
                                }
5046
5047
                                $ok = FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database);
5048
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
5049
                                $listCorrectAnswers['student_score'][$i]  = $ok ? 1 : 0;
5050
                            }
5051
                        } else {
5052
                            // Switchable answers: any student entry can match any teacher word once
5053
                            $studentTemp = (array) ($choice ?? []);
5054
                            $teacherTemp = $info['words'];
5055
5056
                            $count = max(count($studentTemp), count($teacherTemp));
5057
                            for ($i = 0; $i < $count; $i++) {
5058
                                $studentAnswer = isset($studentTemp[$i]) ? trim($studentTemp[$i]) : '';
5059
                                $studentAnswerToShow = $studentAnswer;
5060
5061
                                if ($studentAnswer !== '') {
5062
                                    if (!$from_database) {
5063
                                        $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
5064
                                    }
5065
                                    $found = false;
5066
                                    foreach ($teacherTemp as $j => $correctAnswer) {
5067
                                        if ($found) {
5068
                                            continue;
5069
                                        }
5070
                                        if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
5071
                                            // consume this teacher slot
5072
                                            $teacherTemp[$j] = '';
5073
                                            $found = true;
5074
                                        }
5075
                                    }
5076
5077
                                    // Resolve menu placeholder for display
5078
                                    if ($studentAnswerToShow !== '') {
5079
                                        $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer ?? '');
5080
                                        if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
5081
                                            $menu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
5082
                                            foreach ($menu as $opt) {
5083
                                                if (sha1($opt) === $studentAnswer) {
5084
                                                    $studentAnswerToShow = $opt;
5085
                                                    break;
5086
                                                }
5087
                                            }
5088
                                        }
5089
                                    }
5090
5091
                                    $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
5092
                                    $listCorrectAnswers['student_score'][$i]  = $found ? 1 : 0;
5093
                                } else {
5094
                                    $listCorrectAnswers['student_answer'][$i] = '';
5095
                                    $listCorrectAnswers['student_score'][$i]  = 0;
5096
                                }
5097
                            }
5098
                        }
5099
5100
                        // Build the annotated answer string for display
5101
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
5102
                    break;
5103
                case HOT_SPOT_COMBINATION:
5104
                        if ($from_database) {
5105
                            $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5106
                            // Try by auto id
5107
                            $sql = "SELECT hotspot_correct
5108
                                FROM $TBL_TRACK_HOTSPOT
5109
                                WHERE hotspot_exe_id = $exeId
5110
                                  AND hotspot_question_id = $questionId
5111
                                  AND hotspot_answer_id = $answerAutoId
5112
                                ORDER BY hotspot_id ASC";
5113
                            $result = Database::query($sql);
5114
                            if (Database::num_rows($result)) {
5115
                                $studentChoice = (int) Database::result($result, 0, 'hotspot_correct');
5116
                                $choice[$answerAutoId] = $studentChoice ? 1 : 0;
5117
                            } else {
5118
                                // Fallback to legacy ids
5119
                                $studentChoice = 0;
5120
                                if (!empty($answerIid)) {
5121
                                    $sql = "SELECT hotspot_correct
5122
                                        FROM $TBL_TRACK_HOTSPOT
5123
                                        WHERE hotspot_exe_id = $exeId
5124
                                          AND hotspot_question_id = $questionId
5125
                                          AND hotspot_answer_id = $answerIid
5126
                                        ORDER BY hotspot_id ASC";
5127
                                    $result = Database::query($sql);
5128
                                    if (Database::num_rows($result)) {
5129
                                        $studentChoice = (int) Database::result($result, 0, 'hotspot_correct');
5130
                                    }
5131
                                }
5132
                                $choice[$answerAutoId] = $studentChoice ? 1 : 0;
5133
                                $choice[$answerIid]    = $choice[$answerAutoId];
5134
                            }
5135
                        } else {
5136
                            // Validate coordinate inside the expected shape and mark $choice
5137
                            if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
5138
                                $choice[$answerAutoId] = 0;
5139
                                $choice[$answerIid]    = 0;
5140
                                $studentChoice = 0;
5141
                            } else {
5142
                                $studentChoicePoint = $choice[$answerAutoId] ?? $choice[$answerIid] ?? '';
5143
                                $isValid = false;
5144
5145
                                if (!empty($studentChoicePoint)) {
5146
                                    $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
5147
                                    $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
5148
                                    $choicePoint = Geometry::decodePoint($studentChoicePoint);
5149
5150
                                    switch ($hotspotType) {
5151
                                        case 'square':
5152
                                            $props = Geometry::decodeSquare($hotspotCoordinates);
5153
                                            $isValid = Geometry::pointIsInSquare($props, $choicePoint);
5154
                                            break;
5155
                                        case 'circle':
5156
                                            $props = Geometry::decodeEllipse($hotspotCoordinates);
5157
                                            $isValid = Geometry::pointIsInEllipse($props, $choicePoint);
5158
                                            break;
5159
                                        case 'poly':
5160
                                            $props = Geometry::decodePolygon($hotspotCoordinates);
5161
                                            $isValid = Geometry::pointIsInPolygon($props, $choicePoint);
5162
                                            break;
5163
                                    }
5164
                                }
5165
5166
                                $choice[$answerAutoId] = $isValid ? 1 : 0;
5167
                                $choice[$answerIid]    = $choice[$answerAutoId];
5168
                                $studentChoice         = $choice[$answerAutoId];
5169
                            }
5170
                        }
5171
                    break;
5172
                case ANNOTATION:
5173
                    if ($from_database) {
5174
                        $sql = "SELECT answer, marks
5175
                                FROM $TBL_TRACK_ATTEMPT
5176
                                WHERE
5177
                                  exe_id = $exeId AND
5178
                                  question_id = $questionId ";
5179
                        $resq = Database::query($sql);
5180
                        $data = Database::fetch_array($resq);
5181
5182
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
5183
                        $arrques = $questionName;
5184
5185
                        break;
5186
                    }
5187
                    $studentChoice = $choice;
5188
                    if ($studentChoice) {
5189
                        $questionScore = 0;
5190
                    }
5191
5192
                    break;
5193
            }
5194
5195
            if ($show_result) {
5196
                if ('exercise_result' === $from) {
5197
                    // Display answers (if not matching type, or if the answer is correct)
5198
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION]) ||
5199
                        $answerCorrect
5200
                    ) {
5201
                        if (in_array(
5202
                            $answerType,
5203
                            [
5204
                                UNIQUE_ANSWER,
5205
                                UNIQUE_ANSWER_IMAGE,
5206
                                UNIQUE_ANSWER_NO_OPTION,
5207
                                MULTIPLE_ANSWER,
5208
                                MULTIPLE_ANSWER_COMBINATION,
5209
                                GLOBAL_MULTIPLE_ANSWER,
5210
                                READING_COMPREHENSION,
5211
                            ]
5212
                        )) {
5213
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
5214
                                $this,
5215
                                $feedback_type,
5216
                                $answerType,
5217
                                $studentChoice,
5218
                                $answer,
5219
                                $answerComment,
5220
                                $answerCorrect,
5221
                                0,
5222
                                0,
5223
                                0,
5224
                                $results_disabled,
5225
                                $showTotalScoreAndUserChoicesInLastAttempt,
5226
                                $this->export
5227
                            );
5228
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
5229
                            ExerciseShowFunctions::display_multiple_answer_true_false(
5230
                                $this,
5231
                                $feedback_type,
5232
                                $answerType,
5233
                                $studentChoice,
5234
                                $answer,
5235
                                $answerComment,
5236
                                $answerCorrect,
5237
                                0,
5238
                                $questionId,
5239
                                0,
5240
                                $results_disabled,
5241
                                $showTotalScoreAndUserChoicesInLastAttempt
5242
                            );
5243
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
5244
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5245
                                $this,
5246
                                $feedback_type,
5247
                                $studentChoice,
5248
                                $studentChoiceDegree,
5249
                                $answer,
5250
                                $answerComment,
5251
                                $answerCorrect,
5252
                                $questionId,
5253
                                $results_disabled
5254
                            );
5255
                        } elseif (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) {
5256
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5257
                                $this,
5258
                                $feedback_type,
5259
                                $answerType,
5260
                                $studentChoice,
5261
                                $answer,
5262
                                $answerComment,
5263
                                $answerCorrect,
5264
                                0,
5265
                                0,
5266
                                0,
5267
                                $results_disabled,
5268
                                $showTotalScoreAndUserChoicesInLastAttempt
5269
                            );
5270
                        } elseif (in_array($answerType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
5271
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5272
                                $this,
5273
                                $feedback_type,
5274
                                $answer,
5275
                                0,
5276
                                0,
5277
                                $results_disabled,
5278
                                $showTotalScoreAndUserChoicesInLastAttempt,
5279
                                ''
5280
                            );
5281
                        } elseif (CALCULATED_ANSWER == $answerType) {
5282
                            ExerciseShowFunctions::display_calculated_answer(
5283
                                $this,
5284
                                $feedback_type,
5285
                                $answer,
5286
                                0,
5287
                                0,
5288
                                $results_disabled,
5289
                                $showTotalScoreAndUserChoicesInLastAttempt,
5290
                                $expectedAnswer,
5291
                                $calculatedChoice,
5292
                                $calculatedStatus
5293
                            );
5294
                        } elseif (FREE_ANSWER == $answerType) {
5295
                            ExerciseShowFunctions::display_free_answer(
5296
                                $feedback_type,
5297
                                $choice,
5298
                                $exeId,
5299
                                $questionId,
5300
                                $questionScore,
5301
                                $results_disabled
5302
                            );
5303
                        } elseif ($answerType == UPLOAD_ANSWER) {
5304
                            ExerciseShowFunctions::displayUploadAnswer(
5305
                                $feedback_type,
5306
                                $choice,
5307
                                $exeId,
5308
                                $questionId,
5309
                                $questionScore,
5310
                                $results_disabled
5311
                            );
5312
                        } elseif (ORAL_EXPRESSION == $answerType) {
5313
                            // to store the details of open questions in an array to be used in mail
5314
                            /** @var OralExpression $objQuestionTmp */
5315
                            ExerciseShowFunctions::display_oral_expression_answer(
5316
                                $feedback_type,
5317
                                $choice,
5318
                                $exeId,
5319
                                $questionId,
5320
                                $results_disabled,
5321
                                $questionScore,
5322
                                true
5323
                            );
5324
                        } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION], true)) {
5325
                            $correctAnswerId = 0;
5326
                            /** @var TrackEHotspot $hotspot */
5327
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5328
                                if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
5329
                                    break;
5330
                                }
5331
                            }
5332
5333
                            // force to show whether the choice is correct or not
5334
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5335
                            ExerciseShowFunctions::display_hotspot_answer(
5336
                                $this,
5337
                                $feedback_type,
5338
                                $answerId,
5339
                                $answer,
5340
                                $studentChoice,
5341
                                $answerComment,
5342
                                $results_disabled,
5343
                                $answerId,
5344
                                $showTotalScoreAndUserChoicesInLastAttempt
5345
                            );
5346
                        } elseif (HOT_SPOT_ORDER == $answerType) {
5347
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5348
                                $feedback_type,
5349
                                $answerId,
5350
                                $answer,
5351
                                $studentChoice,
5352
                                $answerComment
5353
                            );*/
5354
                        } elseif (HOT_SPOT_DELINEATION == $answerType && isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5355
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5356
5357
                            // Round-up the coordinates
5358
                            $coords = explode('/', $user_answer);
5359
                            $coords = array_filter($coords);
5360
                            $user_array = '';
5361
                            foreach ($coords as $coord) {
5362
                                if (!empty($coord)) {
5363
                                    $parts = explode(';', $coord);
5364
                                    if (!empty($parts)) {
5365
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5366
                                    }
5367
                                }
5368
                            }
5369
                            $user_array = substr($user_array, 0, -1) ?: '';
5370
                            if ($next) {
5371
                                $user_answer = $user_array;
5372
                                // We compare only the delineation not the other points
5373
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5374
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5375
5376
                                // Calculating the area
5377
                                $poly_user = convert_coordinates($user_answer, '/');
5378
                                $poly_answer = convert_coordinates($answer_question, '|');
5379
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5380
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5381
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5382
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5383
5384
                                $overlap = $poly_results['both'];
5385
                                $poly_answer_area = $poly_results['s1'];
5386
                                $poly_user_area = $poly_results['s2'];
5387
                                $missing = $poly_results['s1Only'];
5388
                                $excess = $poly_results['s2Only'];
5389
5390
                                // //this is an area in pixels
5391
                                if ($debug > 0) {
5392
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5393
                                }
5394
5395
                                if ($overlap < 1) {
5396
                                    // Shortcut to avoid complicated calculations
5397
                                    $final_overlap = 0;
5398
                                    $final_missing = 100;
5399
                                    $final_excess = 100;
5400
                                } else {
5401
                                    // the final overlap is the percentage of the initial polygon
5402
                                    // that is overlapped by the user's polygon
5403
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5404
                                    if ($debug > 1) {
5405
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5406
                                    }
5407
                                    // the final missing area is the percentage of the initial polygon
5408
                                    // that is not overlapped by the user's polygon
5409
                                    $final_missing = 100 - $final_overlap;
5410
                                    if ($debug > 1) {
5411
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5412
                                    }
5413
                                    // the final excess area is the percentage of the initial polygon's size
5414
                                    // that is covered by the user's polygon outside of the initial polygon
5415
                                    $final_excess = round(
5416
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5417
                                    );
5418
                                    if ($debug > 1) {
5419
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5420
                                    }
5421
                                }
5422
5423
                                // Checking the destination parameters parsing the "@@"
5424
                                $destination_items = explode('@@', $answerDestination);
5425
                                $threadhold_total = $destination_items[0];
5426
                                $threadhold_items = explode(';', $threadhold_total);
5427
                                $threadhold1 = $threadhold_items[0]; // overlap
5428
                                $threadhold2 = $threadhold_items[1]; // excess
5429
                                $threadhold3 = $threadhold_items[2]; // missing
5430
5431
                                // if is delineation
5432
                                if (1 === $answerId) {
5433
                                    //setting colors
5434
                                    if ($final_overlap >= $threadhold1) {
5435
                                        $overlap_color = true;
5436
                                    }
5437
                                    if ($final_excess <= $threadhold2) {
5438
                                        $excess_color = true;
5439
                                    }
5440
                                    if ($final_missing <= $threadhold3) {
5441
                                        $missing_color = true;
5442
                                    }
5443
5444
                                    // if pass
5445
                                    if ($final_overlap >= $threadhold1 &&
5446
                                        $final_missing <= $threadhold3 &&
5447
                                        $final_excess <= $threadhold2
5448
                                    ) {
5449
                                        $next = 1; //go to the oars
5450
                                        $result_comment = get_lang('Acceptable');
5451
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5452
                                    } else {
5453
                                        $next = 0;
5454
                                        $result_comment = get_lang('Unacceptable');
5455
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5456
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5457
                                        // checking the destination parameters parsing the "@@"
5458
                                        $destination_items = explode('@@', $answerDestination);
5459
                                    }
5460
                                } elseif ($answerId > 1) {
5461
                                    if ('noerror' == $objAnswerTmp->selectHotspotType($answerId)) {
5462
                                        if ($debug > 0) {
5463
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5464
                                        }
5465
                                        //type no error shouldn't be treated
5466
                                        $next = 1;
5467
5468
                                        continue;
5469
                                    }
5470
                                    if ($debug > 0) {
5471
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5472
                                    }
5473
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5474
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5475
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5476
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5477
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5478
5479
                                    if (false == $overlap) {
5480
                                        //all good, no overlap
5481
                                        $next = 1;
5482
5483
                                        continue;
5484
                                    } else {
5485
                                        if ($debug > 0) {
5486
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5487
                                        }
5488
                                        $organs_at_risk_hit++;
5489
                                        //show the feedback
5490
                                        $next = 0;
5491
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5492
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5493
5494
                                        $destination_items = explode('@@', $answerDestination);
5495
                                        $try_hotspot = $destination_items[1];
5496
                                        $lp_hotspot = $destination_items[2];
5497
                                        $select_question_hotspot = $destination_items[3];
5498
                                        $url_hotspot = $destination_items[4];
5499
                                    }
5500
                                }
5501
                            } else {
5502
                                // the first delineation feedback
5503
                                if ($debug > 0) {
5504
                                    error_log(__LINE__.' first', 0);
5505
                                }
5506
                            }
5507
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
5508
                            echo '<tr>';
5509
                            echo Display::tag('td', $answerMatching[$answerId]);
5510
                            echo Display::tag(
5511
                                'td',
5512
                                "$user_answer / ".Display::tag(
5513
                                    'strong',
5514
                                    $answerMatching[$answerCorrect],
5515
                                    ['style' => 'color: #008000; font-weight: bold;']
5516
                                )
5517
                            );
5518
                            echo '</tr>';
5519
                        } elseif (ANNOTATION == $answerType) {
5520
                            ExerciseShowFunctions::displayAnnotationAnswer(
5521
                                $feedback_type,
5522
                                $exeId,
5523
                                $questionId,
5524
                                $questionScore,
5525
                                $results_disabled
5526
                            );
5527
                        }
5528
                    }
5529
                } else {
5530
                    if ($debug) {
5531
                        error_log('Showing questions $from '.$from);
5532
                    }
5533
5534
                    switch ($answerType) {
5535
                        case UNIQUE_ANSWER:
5536
                        case UNIQUE_ANSWER_IMAGE:
5537
                        case UNIQUE_ANSWER_NO_OPTION:
5538
                        case MULTIPLE_ANSWER:
5539
                        case GLOBAL_MULTIPLE_ANSWER:
5540
                        case MULTIPLE_ANSWER_COMBINATION:
5541
                        case READING_COMPREHENSION:
5542
                            if (1 == $answerId) {
5543
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5544
                                    $this,
5545
                                    $feedback_type,
5546
                                    $answerType,
5547
                                    $studentChoice,
5548
                                    $answer,
5549
                                    $answerComment,
5550
                                    $answerCorrect,
5551
                                    $exeId,
5552
                                    $questionId,
5553
                                    $answerId,
5554
                                    $results_disabled,
5555
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5556
                                    $this->export
5557
                                );
5558
                            } else {
5559
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5560
                                    $this,
5561
                                    $feedback_type,
5562
                                    $answerType,
5563
                                    $studentChoice,
5564
                                    $answer,
5565
                                    $answerComment,
5566
                                    $answerCorrect,
5567
                                    $exeId,
5568
                                    $questionId,
5569
                                    '',
5570
                                    $results_disabled,
5571
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5572
                                    $this->export
5573
                                );
5574
                            }
5575
5576
                            break;
5577
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5578
                            if (1 == $answerId) {
5579
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5580
                                    $this,
5581
                                    $feedback_type,
5582
                                    $answerType,
5583
                                    $studentChoice,
5584
                                    $answer,
5585
                                    $answerComment,
5586
                                    $answerCorrect,
5587
                                    $exeId,
5588
                                    $questionId,
5589
                                    $answerId,
5590
                                    $results_disabled,
5591
                                    $showTotalScoreAndUserChoicesInLastAttempt
5592
                                );
5593
                            } else {
5594
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5595
                                    $this,
5596
                                    $feedback_type,
5597
                                    $answerType,
5598
                                    $studentChoice,
5599
                                    $answer,
5600
                                    $answerComment,
5601
                                    $answerCorrect,
5602
                                    $exeId,
5603
                                    $questionId,
5604
                                    '',
5605
                                    $results_disabled,
5606
                                    $showTotalScoreAndUserChoicesInLastAttempt
5607
                                );
5608
                            }
5609
5610
                            break;
5611
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5612
                            if (1 == $answerId) {
5613
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5614
                                    $this,
5615
                                    $feedback_type,
5616
                                    $answerType,
5617
                                    $studentChoice,
5618
                                    $answer,
5619
                                    $answerComment,
5620
                                    $answerCorrect,
5621
                                    $exeId,
5622
                                    $questionId,
5623
                                    $answerId,
5624
                                    $results_disabled,
5625
                                    $showTotalScoreAndUserChoicesInLastAttempt
5626
                                );
5627
                            } else {
5628
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5629
                                    $this,
5630
                                    $feedback_type,
5631
                                    $answerType,
5632
                                    $studentChoice,
5633
                                    $answer,
5634
                                    $answerComment,
5635
                                    $answerCorrect,
5636
                                    $exeId,
5637
                                    $questionId,
5638
                                    '',
5639
                                    $results_disabled,
5640
                                    $showTotalScoreAndUserChoicesInLastAttempt
5641
                                );
5642
                            }
5643
5644
                            break;
5645
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5646
                            if (1 == $answerId) {
5647
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5648
                                    $this,
5649
                                    $feedback_type,
5650
                                    $studentChoice,
5651
                                    $studentChoiceDegree,
5652
                                    $answer,
5653
                                    $answerComment,
5654
                                    $answerCorrect,
5655
                                    $questionId,
5656
                                    $results_disabled
5657
                                );
5658
                            } else {
5659
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5660
                                    $this,
5661
                                    $feedback_type,
5662
                                    $studentChoice,
5663
                                    $studentChoiceDegree,
5664
                                    $answer,
5665
                                    $answerComment,
5666
                                    $answerCorrect,
5667
                                    $questionId,
5668
                                    $results_disabled
5669
                                );
5670
                            }
5671
5672
                            break;
5673
                        case FILL_IN_BLANKS:
5674
                        case FILL_IN_BLANKS_COMBINATION:
5675
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5676
                                $this,
5677
                                $feedback_type,
5678
                                $answer,
5679
                                $exeId,
5680
                                $questionId,
5681
                                $results_disabled,
5682
                                $showTotalScoreAndUserChoicesInLastAttempt,
5683
                                $str
5684
                            );
5685
                            break;
5686
                        case CALCULATED_ANSWER:
5687
                            ExerciseShowFunctions::display_calculated_answer(
5688
                                $this,
5689
                                $feedback_type,
5690
                                $answer,
5691
                                $exeId,
5692
                                $questionId,
5693
                                $results_disabled,
5694
                                '',
5695
                                $showTotalScoreAndUserChoicesInLastAttempt
5696
                            );
5697
5698
                            break;
5699
                        case FREE_ANSWER:
5700
                            echo ExerciseShowFunctions::display_free_answer(
5701
                                $feedback_type,
5702
                                $choice,
5703
                                $exeId,
5704
                                $questionId,
5705
                                $questionScore,
5706
                                $results_disabled
5707
                            );
5708
5709
                            break;
5710
                        case UPLOAD_ANSWER:
5711
                            echo ExerciseShowFunctions::displayUploadAnswer(
5712
                                $feedback_type,
5713
                                $choice,
5714
                                $exeId,
5715
                                $questionId,
5716
                                $questionScore,
5717
                                $results_disabled
5718
                            );
5719
                            break;
5720
                        case ORAL_EXPRESSION:
5721
                            /** @var OralExpression $objQuestionTmp */
5722
                            echo '<tr>
5723
                                <td valign="top">'.
5724
                                ExerciseShowFunctions::display_oral_expression_answer(
5725
                                    $feedback_type,
5726
                                    $choice,
5727
                                    $exeId,
5728
                                    $questionId,
5729
                                    $results_disabled,
5730
                                    $questionScore
5731
                                ).'</td>
5732
                                </tr>
5733
                                </table>';
5734
                            break;
5735
                        case HOT_SPOT:
5736
                        case HOT_SPOT_COMBINATION:
5737
                            $correctAnswerId = 0;
5738
5739
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5740
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5741
                                    break;
5742
                                }
5743
                            }
5744
                            ExerciseShowFunctions::display_hotspot_answer(
5745
                                $this,
5746
                                $feedback_type,
5747
                                $answerId,
5748
                                $answer,
5749
                                $studentChoice,
5750
                                $answerComment,
5751
                                $results_disabled,
5752
                                $answerId,
5753
                                $showTotalScoreAndUserChoicesInLastAttempt
5754
                            );
5755
5756
                            break;
5757
                        case HOT_SPOT_DELINEATION:
5758
                            $user_answer = $user_array;
5759
                            if ($next) {
5760
                                $user_answer = $user_array;
5761
                                // we compare only the delineation not the other points
5762
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5763
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5764
5765
                                // calculating the area
5766
                                $poly_user = convert_coordinates($user_answer, '/');
5767
                                $poly_answer = convert_coordinates($answer_question, '|');
5768
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5769
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5770
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5771
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5772
5773
                                $overlap = $poly_results['both'];
5774
                                $poly_answer_area = $poly_results['s1'];
5775
                                $poly_user_area = $poly_results['s2'];
5776
                                $missing = $poly_results['s1Only'];
5777
                                $excess = $poly_results['s2Only'];
5778
                                if ($debug > 0) {
5779
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5780
                                }
5781
                                if ($overlap < 1) {
5782
                                    //shortcut to avoid complicated calculations
5783
                                    $final_overlap = 0;
5784
                                    $final_missing = 100;
5785
                                    $final_excess = 100;
5786
                                } else {
5787
                                    // the final overlap is the percentage of the initial polygon
5788
                                    // that is overlapped by the user's polygon
5789
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5790
5791
                                    // the final missing area is the percentage of the initial polygon that
5792
                                    // is not overlapped by the user's polygon
5793
                                    $final_missing = 100 - $final_overlap;
5794
                                    // the final excess area is the percentage of the initial polygon's size that is
5795
                                    // covered by the user's polygon outside of the initial polygon
5796
                                    $final_excess = round(
5797
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5798
                                    );
5799
5800
                                    if ($debug > 1) {
5801
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5802
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5803
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5804
                                    }
5805
                                }
5806
5807
                                // Checking the destination parameters parsing the "@@"
5808
                                $destination_items = explode('@@', $answerDestination);
5809
                                $threadhold_total = $destination_items[0];
5810
                                $threadhold_items = explode(';', $threadhold_total);
5811
                                $threadhold1 = $threadhold_items[0]; // overlap
5812
                                $threadhold2 = $threadhold_items[1]; // excess
5813
                                $threadhold3 = $threadhold_items[2]; //missing
5814
                                // if is delineation
5815
                                if (1 === $answerId) {
5816
                                    //setting colors
5817
                                    if ($final_overlap >= $threadhold1) {
5818
                                        $overlap_color = true;
5819
                                    }
5820
                                    if ($final_excess <= $threadhold2) {
5821
                                        $excess_color = true;
5822
                                    }
5823
                                    if ($final_missing <= $threadhold3) {
5824
                                        $missing_color = true;
5825
                                    }
5826
5827
                                    // if pass
5828
                                    if ($final_overlap >= $threadhold1 &&
5829
                                        $final_missing <= $threadhold3 &&
5830
                                        $final_excess <= $threadhold2
5831
                                    ) {
5832
                                        $next = 1; //go to the oars
5833
                                        $result_comment = get_lang('Acceptable');
5834
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5835
                                    } else {
5836
                                        $next = 0;
5837
                                        $result_comment = get_lang('Unacceptable');
5838
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5839
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5840
                                        //checking the destination parameters parsing the "@@"
5841
                                        $destination_items = explode('@@', $answerDestination);
5842
                                    }
5843
                                } elseif ($answerId > 1) {
5844
                                    if ('noerror' === $objAnswerTmp->selectHotspotType($answerId)) {
5845
                                        if ($debug > 0) {
5846
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5847
                                        }
5848
                                        //type no error shouldn't be treated
5849
                                        $next = 1;
5850
5851
                                        break;
5852
                                    }
5853
                                    if ($debug > 0) {
5854
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5855
                                    }
5856
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5857
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5858
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5859
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5860
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5861
5862
                                    if (false == $overlap) {
5863
                                        //all good, no overlap
5864
                                        $next = 1;
5865
5866
                                        break;
5867
                                    } else {
5868
                                        if ($debug > 0) {
5869
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5870
                                        }
5871
                                        $organs_at_risk_hit++;
5872
                                        //show the feedback
5873
                                        $next = 0;
5874
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5875
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5876
5877
                                        $destination_items = explode('@@', $answerDestination);
5878
                                        $try_hotspot = $destination_items[1];
5879
                                        $lp_hotspot = $destination_items[2];
5880
                                        $select_question_hotspot = $destination_items[3];
5881
                                        $url_hotspot = $destination_items[4];
5882
                                    }
5883
                                }
5884
                            }
5885
5886
                            break;
5887
                        case HOT_SPOT_ORDER:
5888
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5889
                                $feedback_type,
5890
                                $answerId,
5891
                                $answer,
5892
                                $studentChoice,
5893
                                $answerComment
5894
                            );*/
5895
5896
                            break;
5897
                        case DRAGGABLE:
5898
                        case MATCHING_DRAGGABLE:
5899
                        case MATCHING:
5900
                        case MATCHING_COMBINATION:
5901
                        case MATCHING_DRAGGABLE_COMBINATION:
5902
                            echo '<tr>';
5903
                            echo Display::tag('td', $answerMatching[$answerId]);
5904
                            echo Display::tag(
5905
                                'td',
5906
                                "$user_answer / ".Display::tag(
5907
                                    'strong',
5908
                                    $answerMatching[$answerCorrect],
5909
                                    ['style' => 'color: #008000; font-weight: bold;']
5910
                                )
5911
                            );
5912
                            echo '</tr>';
5913
5914
                            break;
5915
                        case ANNOTATION:
5916
                            ExerciseShowFunctions::displayAnnotationAnswer(
5917
                                $feedback_type,
5918
                                $exeId,
5919
                                $questionId,
5920
                                $questionScore,
5921
                                $results_disabled
5922
                            );
5923
5924
                            break;
5925
                    }
5926
                }
5927
            }
5928
        } // end for that loops over all answers of the current question
5929
5930
        if ($debug) {
5931
            error_log('-- End answer loop --');
5932
        }
5933
5934
        $final_answer = true;
5935
5936
        foreach ($real_answers as $my_answer) {
5937
            if (!$my_answer) {
5938
                $final_answer = false;
5939
            }
5940
        }
5941
5942
        if (FILL_IN_BLANKS_COMBINATION === $answerType) {
5943
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5944
                $answerType,
5945
                $listCorrectAnswers,
5946
                $exeId,
5947
                $questionId,
5948
                $questionWeighting
5949
            );
5950
        }
5951
5952
        if (HOT_SPOT_COMBINATION === $answerType) {
5953
            $listCoords = $exerciseResultCoordinates[$questionId] ?? [];
5954
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5955
                $answerType,
5956
                $listCoords,
5957
                $exeId,
5958
                $questionId,
5959
                $questionWeighting,
5960
                (array) ($choice ?? []),
5961
                $nbrAnswers
5962
            );
5963
        }
5964
5965
        if (in_array($answerType, [MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION], true)) {
5966
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5967
                $answerType,
5968
                $matchingCorrectAnswers[$questionId] ?? [],
5969
                $exeId,
5970
                $questionId,
5971
                $questionWeighting
5972
            );
5973
        }
5974
5975
        //we add the total score after dealing with the answers
5976
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
5977
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
5978
        ) {
5979
            if ($final_answer) {
5980
                //getting only the first score where we save the weight of all the question
5981
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5982
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
5983
                    $answerWeighting = $firstAnswer['ponderation'];
5984
                }
5985
                $questionScore += $answerWeighting;
5986
            }
5987
        }
5988
5989
        $extra_data = [
5990
            'final_overlap' => $final_overlap,
5991
            'final_missing' => $final_missing,
5992
            'final_excess' => $final_excess,
5993
            'overlap_color' => $overlap_color,
5994
            'missing_color' => $missing_color,
5995
            'excess_color' => $excess_color,
5996
            'threadhold1' => $threadhold1,
5997
            'threadhold2' => $threadhold2,
5998
            'threadhold3' => $threadhold3,
5999
        ];
6000
6001
        if ('exercise_result' === $from) {
6002
            // if answer is hotspot. To the difference of exercise_show.php,
6003
            //  we use the results from the session (from_db=0)
6004
            // TODO Change this, because it is wrong to show the user
6005
            //  some results that haven't been stored in the database yet
6006
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_DELINEATION, HOT_SPOT_COMBINATION])) {
6007
                if ($debug) {
6008
                    error_log('$from AND this is a hotspot kind of question ');
6009
                }
6010
                if (HOT_SPOT_DELINEATION === $answerType) {
6011
                    if ($showHotSpotDelineationTable) {
6012
                        if (!is_numeric($final_overlap)) {
6013
                            $final_overlap = 0;
6014
                        }
6015
                        if (!is_numeric($final_missing)) {
6016
                            $final_missing = 0;
6017
                        }
6018
                        if (!is_numeric($final_excess)) {
6019
                            $final_excess = 0;
6020
                        }
6021
6022
                        if ($final_overlap > 100) {
6023
                            $final_overlap = 100;
6024
                        }
6025
6026
                        $overlap = 0;
6027
                        if ($final_overlap > 0) {
6028
                            $overlap = (int) $final_overlap;
6029
                        }
6030
6031
                        $excess = 0;
6032
                        if ($final_excess > 0) {
6033
                            $excess = (int) $final_excess;
6034
                        }
6035
6036
                        $missing = 0;
6037
                        if ($final_missing > 0) {
6038
                            $missing = (int) $final_missing;
6039
                        }
6040
6041
                        $table_resume = '<table class="table table-hover table-striped data_table">
6042
                                <tr class="row_odd" >
6043
                                    <td></td>
6044
                                    <td ><b>'.get_lang('Requirements').'</b></td>
6045
                                    <td><b>'.get_lang('Your answer').'</b></td>
6046
                                </tr>
6047
                                <tr class="row_even">
6048
                                    <td><b>'.get_lang('Overlapping area').'</b></td>
6049
                                    <td>'.get_lang('Minimum').' '.$threadhold1.'</td>
6050
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-error').'">'
6051
                                    .$overlap.'</td>
6052
                                </tr>
6053
                                <tr>
6054
                                    <td><b>'.get_lang('Excessive area').'</b></td>
6055
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
6056
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-error').'">'
6057
                                    .$excess.'</td>
6058
                                </tr>
6059
                                <tr class="row_even">
6060
                                    <td><b>'.get_lang('Missing area').'</b></td>
6061
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
6062
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-error').'">'
6063
                                    .$missing.'</td>
6064
                                </tr>
6065
                            </table>';
6066
                        if (0 == $next) {
6067
                        } else {
6068
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
6069
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
6070
                        }
6071
6072
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
6073
                                    <p style="text-align:center">';
6074
                        $message .= '<p>'.get_lang('Your delineation :').'</p>';
6075
                        $message .= $table_resume;
6076
                        $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
6077
                        if ($organs_at_risk_hit > 0) {
6078
                            $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
6079
                        }
6080
                        $message .= '<p>'.$comment.'</p>';
6081
                        echo $message;
6082
6083
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][0] = $message;
6084
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
6085
                            $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
6086
                        }
6087
                    } else {
6088
                        echo $hotspot_delineation_result[0] ?? '';
6089
                    }
6090
6091
                    // Save the score attempts
6092
                    if (1) {
6093
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
6094
                        $final_answer = $hotspot_delineation_result[1] ?? '';
6095
                        if (0 == $final_answer) {
6096
                            $questionScore = 0;
6097
                        }
6098
                        // we always insert the answer_id 1 = delineation
6099
                        Event::saveQuestionAttempt($this, $questionScore, 1, $quesId, $exeId, 0);
6100
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
6101
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? 1 === (int) $hotspot_delineation_result[1] ? 1 : 0 : 0;
6102
                        Event::saveExerciseAttemptHotspot(
6103
                            $this,
6104
                            $exeId,
6105
                            $quesId,
6106
                            1,
6107
                            $hotspotValue,
6108
                            $exerciseResultCoordinates[$quesId] ?? '',
6109
                            false,
6110
                            0,
6111
                            $learnpath_id,
6112
                            $learnpath_item_id
6113
                        );
6114
                    } else {
6115
                        if (0 == $final_answer) {
6116
                            $questionScore = 0;
6117
                            $answer = 0;
6118
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
6119
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6120
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6121
                                    Event::saveExerciseAttemptHotspot(
6122
                                        $this,
6123
                                        $exeId,
6124
                                        $quesId,
6125
                                        $idx,
6126
                                        0,
6127
                                        $val,
6128
                                        false,
6129
                                        0,
6130
                                        $learnpath_id,
6131
                                        $learnpath_item_id
6132
                                    );
6133
                                }
6134
                            }
6135
                        } else {
6136
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
6137
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6138
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6139
                                    $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6140
                                    Event::saveExerciseAttemptHotspot(
6141
                                        $this,
6142
                                        $exeId,
6143
                                        $quesId,
6144
                                        $idx,
6145
                                        $hotspotValue,
6146
                                        $val,
6147
                                        false,
6148
                                        0,
6149
                                        $learnpath_id,
6150
                                        $learnpath_item_id
6151
                                    );
6152
                                }
6153
                            }
6154
                        }
6155
                    }
6156
                }
6157
            }
6158
6159
            $relPath = api_get_path(WEB_CODE_PATH);
6160
6161
            if ($save_results
6162
                && in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION], true)
6163
                && !empty($exerciseResultCoordinates[$questionId])
6164
            ) {
6165
                Database::delete(
6166
                    Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6167
                    [
6168
                        'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6169
                            $exeId, $questionId, api_get_course_int_id(),
6170
                        ],
6171
                    ]
6172
                );
6173
6174
                foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6175
                    $hotspotValue = !empty($choice[$idx]) ? 1 : 0;
6176
                    Event::saveExerciseAttemptHotspot(
6177
                        $this,
6178
                        $exeId,
6179
                        $quesId,
6180
                        $idx,
6181
                        $hotspotValue,
6182
                        $val,
6183
                        false,
6184
                        $this->id,
6185
                        $learnpath_id,
6186
                        $learnpath_item_id
6187
                    );
6188
                }
6189
                $hotspotsSavedEarly = true;
6190
            }
6191
6192
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_COMBINATION], true)) {
6193
                // We made an extra table for the answers
6194
                if ($show_result) {
6195
                    echo '</table></td></tr>';
6196
                    echo '
6197
                        <tr>
6198
                            <td colspan="2">
6199
                                <p><em>'.get_lang('Image zones')."</em></p>
6200
                                <div id=\"hotspot-solution-$questionId\"></div>
6201
                                <script>
6202
                                    $(function() {
6203
                                        new HotspotQuestion({
6204
                                            questionId: $questionId,
6205
                                            exerciseId: {$this->getId()},
6206
                                            exeId: $exeId,
6207
                                            selector: '#hotspot-solution-$questionId',
6208
                                            for: 'solution',
6209
                                            relPath: '$relPath'
6210
                                        });
6211
                                    });
6212
                                </script>
6213
                            </td>
6214
                        </tr>
6215
                    ";
6216
                }
6217
            } elseif (ANNOTATION == $answerType) {
6218
                if ($show_result) {
6219
                    echo '
6220
                        <p><em>'.get_lang('Annotation').'</em></p>
6221
                        <div id="annotation-canvas-'.$questionId.'"></div>
6222
                        <script>
6223
                            AnnotationQuestion({
6224
                                questionId: parseInt('.$questionId.'),
6225
                                exerciseId: parseInt('.$exeId.'),
6226
                                relPath: \''.$relPath.'\',
6227
                                courseId: parseInt('.$course_id.')
6228
                            });
6229
                        </script>
6230
                    ';
6231
                }
6232
            }
6233
6234
            if ($show_result && ANNOTATION != $answerType) {
6235
                echo '</table>';
6236
            }
6237
        }
6238
        unset($objAnswerTmp);
6239
6240
        $totalWeighting += $questionWeighting;
6241
        // Store results directly in the database
6242
        // For all in one page exercises, the results will be
6243
        // stored by exercise_results.php (using the session)
6244
        if (in_array(
6245
            $objQuestionTmp->type,
6246
            [PAGE_BREAK, MEDIA_QUESTION],
6247
            true
6248
        )) {
6249
            $save_results = false;
6250
        }
6251
        if ($save_results) {
6252
            if ($debug) {
6253
                error_log("Save question results $save_results");
6254
                error_log("Question score: $questionScore");
6255
                error_log('choice: ');
6256
                error_log(print_r($choice, 1));
6257
            }
6258
6259
            if (empty($choice)) {
6260
                $choice = 0;
6261
            }
6262
            // with certainty degree
6263
            if (empty($choiceDegreeCertainty)) {
6264
                $choiceDegreeCertainty = 0;
6265
            }
6266
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
6267
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType ||
6268
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
6269
            ) {
6270
                if (0 != $choice) {
6271
                    $reply = array_keys($choice);
6272
                    $countReply = count($reply);
6273
                    for ($i = 0; $i < $countReply; $i++) {
6274
                        $chosenAnswer = $reply[$i];
6275
                        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
6276
                            if (0 != $choiceDegreeCertainty) {
6277
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
6278
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
6279
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
6280
                                Event::saveQuestionAttempt(
6281
                                    $this,
6282
                                    $questionScore,
6283
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
6284
                                    $quesId,
6285
                                    $exeId,
6286
                                    $i,
6287
                                    $this->getId(),
6288
                                    $updateResults,
6289
                                    $questionDuration
6290
                                );
6291
                            }
6292
                        } else {
6293
                            Event::saveQuestionAttempt(
6294
                                $this,
6295
                                $questionScore,
6296
                                $chosenAnswer.':'.$choice[$chosenAnswer],
6297
                                $quesId,
6298
                                $exeId,
6299
                                $i,
6300
                                $this->getId(),
6301
                                $updateResults,
6302
                                $questionDuration
6303
                            );
6304
                        }
6305
                        if ($debug) {
6306
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
6307
                        }
6308
                    }
6309
                } else {
6310
                    Event::saveQuestionAttempt(
6311
                        $this,
6312
                        $questionScore,
6313
                        0,
6314
                        $quesId,
6315
                        $exeId,
6316
                        0,
6317
                        $this->getId(),
6318
                        false,
6319
                        $questionDuration
6320
                    );
6321
                }
6322
            } elseif (in_array($answerType, [
6323
                MULTIPLE_ANSWER,
6324
                GLOBAL_MULTIPLE_ANSWER,
6325
                MULTIPLE_ANSWER_DROPDOWN,
6326
                MULTIPLE_ANSWER_DROPDOWN_COMBINATION
6327
            ], true)) {
6328
                if (0 != $choice) {
6329
                    $reply = in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION], true)
6330
                        ? array_values((array) $choice)
6331
                        : array_keys((array) $choice);
6332
                    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...
6333
                        $ans = $reply[$i];
6334
                        Event::saveQuestionAttempt(
6335
                            $this,
6336
                            $questionScore,
6337
                            $ans,
6338
                            $quesId,
6339
                            $exeId,
6340
                            $i,
6341
                            $this->id,
6342
                            false,
6343
                            $questionDuration
6344
                        );
6345
                    }
6346
                } else {
6347
                    Event::saveQuestionAttempt(
6348
                        $this,
6349
                        $questionScore,
6350
                        0,
6351
                        $quesId,
6352
                        $exeId,
6353
                        0,
6354
                        $this->id,
6355
                        false,
6356
                        $questionDuration
6357
                    );
6358
                }
6359
            } elseif (MULTIPLE_ANSWER_COMBINATION == $answerType) {
6360
                if (0 != $choice) {
6361
                    $reply = array_keys($choice);
6362
                    for ($i = 0; $i < count($reply); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

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