Issues (2134)

public/main/exercise/admin.php (4 issues)

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Enums\ActionIcon;
6
use ChamiloSession as Session;
7
8
/**
9
 * Exercise administration
10
 * This script allows to manage (create, modify) an exercise and its questions.
11
 *
12
 * Following scripts are includes for a best code understanding :
13
 *
14
 * - exercise.class.php : for the creation of an Exercise object
15
 * - question.class.php : for the creation of a Question object
16
 * - answer.class.php : for the creation of an Answer object
17
 * - exercise.lib.php : functions used in the exercise tool
18
 * - exercise_admin.inc.php : management of the exercise
19
 * - question_admin.inc.php : management of a question (statement & answers)
20
 * - question_list_admin.inc.php : management of the question list
21
 *
22
 * Main variables used in this script :
23
 *
24
 * - $objAnswer : answer object
25
 * - $exerciseId : the exercise ID
26
 * - $picturePath : the path of question pictures
27
 * - $newQuestion : ask to create a new question
28
 * - $modifyQuestion : ID of the question to modify
29
 * - $editQuestion : ID of the question to edit
30
 * - $submitQuestion : ask to save question modifications
31
 * - $cancelQuestion : ask to cancel question modifications
32
 * - $deleteQuestion : ID of the question to delete
33
 * - $moveUp : ID of the question to move up
34
 * - $moveDown : ID of the question to move down
35
 * - $modifyExercise : ID of the exercise to modify
36
 * - $submitExercise : ask to save exercise modifications
37
 * - $cancelExercise : ask to cancel exercise modifications
38
 * - $modifyAnswers : ID of the question which we want to modify answers for
39
 * - $cancelAnswers : ask to cancel answer modifications
40
 * - $buttonBack : ask to go back to the previous page in answers of type "Fill in blanks"
41
 *
42
 * @author Olivier Brouckaert
43
 * Modified by Hubert Borderiou 21-10-2011 Question by category
44
 */
45
require_once __DIR__.'/../inc/global.inc.php';
46
$current_course_tool = TOOL_QUIZ;
47
$this_section = SECTION_COURSES;
48
49
if (isset($_GET['r']) && 1 == $_GET['r']) {
50
    Exercise::cleanSessionVariables();
51
}
52
53
// Access control
54
api_protect_course_script(true);
55
56
$is_allowedToEdit = api_is_allowed_to_edit(null, true, false, false);
57
$sessionId = api_get_session_id();
58
$studentViewActive = api_is_student_view_active();
59
$showPagination = 'true' === api_get_setting('exercise.show_question_pagination');
60
61
if (!$is_allowedToEdit) {
62
    api_not_allowed(true);
63
}
64
65
$exerciseId = isset($_GET['exerciseId']) ? (int) $_GET['exerciseId'] : 0;
66
if (0 === $exerciseId && isset($_POST['exerciseId'])) {
67
    $exerciseId = (int) $_POST['exerciseId'];
68
}
69
70
$newQuestion = $_GET['newQuestion'] ?? 0;
71
$modifyAnswers = $_GET['modifyAnswers'] ?? 0;
72
$editQuestion = $_GET['editQuestion'] ?? 0;
73
$page = isset($_REQUEST['page']) ? max(1, (int) $_REQUEST['page']) : 1;
74
$modifyQuestion = $_GET['modifyQuestion'] ?? 0;
75
$deleteQuestion = $_GET['deleteQuestion'] ?? 0;
76
$cloneQuestion = $_REQUEST['clone_question'] ?? 0;
77
78
if (empty($questionId)) {
79
    $questionId = Session::read('questionId');
80
}
81
if (empty($modifyExercise)) {
82
    $modifyExercise = $_GET['modifyExercise'] ?? null;
83
}
84
85
$fromExercise = $fromExercise ?? null;
86
$cancelExercise = $cancelExercise ?? null;
87
$cancelAnswers = $cancelAnswers ?? null;
88
$modifyIn = $modifyIn ?? null;
89
$cancelQuestion = $cancelQuestion ?? null;
90
91
/* Cleaning all incomplete attempts of the admin/teacher to avoid weird problems
92
   when changing the exercise settings, number of questions, etc */
93
Event::delete_all_incomplete_attempts(
0 ignored issues
show
The method delete_all_incomplete_attempts() does not exist on Event. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

93
Event::/** @scrutinizer ignore-call */ 
94
       delete_all_incomplete_attempts(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
94
    api_get_user_id(),
95
    $exerciseId,
96
    api_get_course_int_id(),
97
    api_get_session_id()
98
);
99
100
// get from session
101
$objExercise = Session::read('objExercise');
102
$objQuestion = Session::read('objQuestion');
103
104
/**
105
 * IMPORTANT:
106
 * admin.php must allow editing/creating questions without an exercise context.
107
 * This is required by the global question bank (and question pool) use-case where
108
 * exerciseId=0 but editQuestion/newQuestion is provided.
109
 *
110
 * Without this, the script redirects to exercise.php because objExercise is empty,
111
 * which makes the behavior depend on an existing session state.
112
 */
113
$isStandaloneQuestionFlow = (0 === (int) $exerciseId) && (
114
        !empty($editQuestion) || !empty($newQuestion)
115
    );
116
117
if ($isStandaloneQuestionFlow && !($objExercise instanceof Exercise)) {
118
    // Create a minimal Exercise context to avoid redirecting to the test list.
119
    $objExercise = new Exercise();
120
    $objExercise->course_id = api_get_course_int_id();
121
    $objExercise->course = api_get_course_info();
122
    $objExercise->sessionId = $sessionId;
123
    Session::write('objExercise', $objExercise);
124
}
125
126
if (isset($_REQUEST['convertAnswer'])) {
127
    $objQuestion = $objQuestion->swapSimpleAnswerTypes();
128
    Session::write('objQuestion', $objQuestion);
129
}
130
131
$objAnswer = Session::read('objAnswer');
132
$_course = api_get_course_info();
133
134
// tables used in the exercise tool.
135
if (!empty($_GET['action']) && 'exportqti2' === $_GET['action'] && !empty($_GET['questionId'])) {
136
    require_once 'export/qti2/qti2_export.php';
137
    $export = export_question_qti($_GET['questionId'], true);
138
    $qid = (int) $_GET['questionId'];
139
    $name = 'qti2_export_'.$qid.'.zip';
140
    $zip = api_create_zip($name);
141
    $zip->addFile("qti2export_$qid.xml", $export);
142
    $zip->finish();
143
    exit;
144
}
145
146
// Exercise object creation.
147
if (!($objExercise instanceof Exercise)) {
148
    // creation of a new exercise if wrong or not specified exercise ID
149
    if ($exerciseId) {
150
        $objExercise = new Exercise();
151
        $parseQuestionList = $showPagination > 0 ? false : true;
152
        if ($editQuestion) {
153
            $parseQuestionList = false;
154
            $showPagination = true;
155
        }
156
        $objExercise->read($exerciseId, $parseQuestionList);
157
        Session::write('objExercise', $objExercise);
158
    }
159
}
160
161
// Exercise can be edited in their course.
162
if (empty($objExercise)) {
163
    Session::erase('objExercise');
164
    header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
165
    exit;
166
}
167
168
// Exercise can be edited in their course.
169
if ($objExercise->sessionId != $sessionId) {
170
    api_not_allowed(true);
171
}
172
173
// doesn't select the exercise ID if we come from the question pool / global bank
174
// (avoid forcing modifyExercise='yes' when exerciseId=0)
175
if ($exerciseId > 0 && !$fromExercise) {
176
    // gets the right exercise ID, and if 0 creates a new exercise
177
    if (!$exerciseId = $objExercise->getId()) {
178
        $modifyExercise = 'yes';
179
    }
180
}
181
182
$nbrQuestions = $objExercise->getQuestionCount();
183
184
// Question object creation.
185
if ($editQuestion || $newQuestion || $modifyQuestion || $modifyAnswers) {
186
    if ($editQuestion || $newQuestion) {
187
        // reads question data
188
        if ($editQuestion) {
189
            // question not found
190
            if (!$objQuestion = Question::read($editQuestion)) {
191
                api_not_allowed(true);
192
            }
193
            // saves the object into the session
194
            Session::write('objQuestion', $objQuestion);
195
        }
196
    }
197
198
    // checks if the object exists
199
    if (is_object($objQuestion)) {
200
        // gets the question ID
201
        $questionId = $objQuestion->getId();
202
    }
203
}
204
205
// if cancelling an exercise
206
if ($cancelExercise) {
207
    // existing exercise
208
    if ($exerciseId) {
209
        unset($modifyExercise);
210
    } else {
211
        // new exercise
212
        // goes back to the exercise list
213
        header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
214
        exit();
215
    }
216
}
217
218
// if cancelling question creation/modification
219
if ($cancelQuestion) {
220
    // if we are creating a new question from the question pool
221
    if (!$exerciseId && !$questionId) {
222
        // goes back to the question pool
223
        header('Location: question_pool.php?'.api_get_cidreq());
224
        exit();
225
    } else {
226
        // goes back to the question viewing
227
        $editQuestion = $modifyQuestion;
228
        unset($newQuestion, $modifyQuestion);
229
    }
230
}
231
232
if (!empty($cloneQuestion)) {
233
    $cloneQuestion = (int) $cloneQuestion;
234
235
    // Determine destination exercise id (if any)
236
    $targetExerciseId = 0;
237
    if (!empty($exerciseId)) {
238
        $targetExerciseId = (int) $exerciseId;
239
    } elseif ($objExercise instanceof Exercise && (int) $objExercise->getId() > 0) {
240
        $targetExerciseId = (int) $objExercise->getId();
241
    }
242
243
    $oldQuestionObj = Question::read($cloneQuestion);
244
    if (!$oldQuestionObj) {
0 ignored issues
show
$oldQuestionObj is of type Question, thus it always evaluated to true.
Loading history...
245
        Display::addFlash(Display::return_message(get_lang('Question not found'), 'error'));
246
        exit;
247
    }
248
249
    $oldQuestionObj->question = $oldQuestionObj->question.' - '.get_lang('Copy');
250
251
    try {
252
        $newId = $oldQuestionObj->duplicate(api_get_course_info());
253
    } catch (Throwable $e) {
254
        Display::addFlash(Display::return_message(get_lang('Error'), 'error'));
255
        exit;
256
    }
257
258
    $newId = (int) $newId;
259
    if ($newId <= 0) {
260
        Display::addFlash(Display::return_message(get_lang('Error'), 'error'));
261
        exit;
262
    }
263
264
    $newQuestionObj = Question::read($newId);
265
    if (!$newQuestionObj) {
0 ignored issues
show
$newQuestionObj is of type Question, thus it always evaluated to true.
Loading history...
266
        Display::addFlash(Display::return_message(get_lang('Error'), 'error'));
267
        exit;
268
    }
269
270
    // Attach to exercise only if we have a valid target exercise id
271
    if ($targetExerciseId > 0) {
272
        $newQuestionObj->addToList($targetExerciseId);
273
    }
274
275
    // Save category to the destination course (always)
276
    if (!empty($oldQuestionObj->category)) {
277
        $newQuestionObj->saveCategory($oldQuestionObj->category);
278
    }
279
280
    // Duplicate answers
281
    try {
282
        $newAnswerObj = new Answer($cloneQuestion);
283
        $newAnswerObj->read();
284
        $newAnswerObj->duplicate($newQuestionObj);
285
    } catch (Throwable $e) {
286
        error_log('[exercise] clone_question exception during answers duplicate(): '.$e->getMessage());
287
    }
288
289
    // Reload exercise if we cloned inside a test
290
    if ($targetExerciseId > 0) {
291
        if (!($objExercise instanceof Exercise)) {
292
            $objExercise = new Exercise();
293
        }
294
        $objExercise->read($targetExerciseId, false);
295
        Session::write('objExercise', $objExercise);
296
    }
297
298
    Display::addFlash(Display::return_message(get_lang('Item copied')));
299
300
    // Redirect depending on context
301
    if ($targetExerciseId > 0) {
302
        header('Location: admin.php?'.api_get_cidreq().'&exerciseId='.$targetExerciseId.'&page='.$page);
303
    } else {
304
        header('Location: question_pool.php?'.api_get_cidreq().'&page='.$page);
305
    }
306
    exit;
307
}
308
309
// if cancelling answer creation/modification
310
if ($cancelAnswers) {
311
    // goes back to the question viewing
312
    $editQuestion = $modifyAnswers;
313
    unset($modifyAnswers);
314
}
315
316
$nameTools = '';
317
// modifies the query string that is used in the link of tool name
318
if ($editQuestion || $modifyQuestion || $newQuestion || $modifyAnswers) {
319
    $nameTools = get_lang('Question / Answer management');
320
}
321
322
if (api_is_in_gradebook()) {
323
    $interbreadcrumb[] = [
324
        'url' => Category::getUrl(),
325
        'name' => get_lang('Assessments'),
326
    ];
327
}
328
329
$interbreadcrumb[] = [
330
    'url' => api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq(),
331
    'name' => get_lang('Tests'),
332
];
333
334
if (isset($_GET['newQuestion']) || isset($_GET['editQuestion'])) {
335
    // When editing a question without a real exercise, avoid relying on selectTitle()
336
    $exerciseTitle = get_lang('Questions');
337
    $exerciseLink = '#';
338
339
    if ($objExercise instanceof Exercise && (int) $objExercise->getId() > 0) {
340
        $exerciseTitle = $objExercise->selectTitle(true);
341
        $exerciseLink = api_get_path(WEB_CODE_PATH).'exercise/admin.php?exerciseId='.$objExercise->getId().'&'.api_get_cidreq();
342
    }
343
344
    $interbreadcrumb[] = [
345
        'url' => $exerciseLink,
346
        'name' => $exerciseTitle,
347
    ];
348
} else {
349
    $interbreadcrumb[] = [
350
        'url' => '#',
351
        'name' => ($objExercise instanceof Exercise) ? $objExercise->selectTitle(true) : get_lang('Tests'),
352
    ];
353
}
354
355
// shows a link to go back to the question pool
356
if (!$exerciseId && $nameTools != get_lang('Tests management')) {
357
    $interbreadcrumb[] = [
358
        'url' => api_get_path(WEB_CODE_PATH)."exercise/question_pool.php?fromExercise=$fromExercise&".api_get_cidreq(),
359
        'name' => get_lang('Recycle existing questions'),
360
    ];
361
}
362
363
// if the question is duplicated, disable the link of tool name
364
if ('thisExercise' === $modifyIn) {
365
    if (!empty($buttonBack)) {
366
        $modifyIn = 'allExercises';
367
    }
368
}
369
370
$htmlHeadXtra[] = api_get_build_js('legacy_exercise.js');
371
372
$template = new Template();
373
$templateName = $template->get_template('exercise/submit.js.tpl');
374
$htmlHeadXtra[] = $template->fetch($templateName);
375
$htmlHeadXtra[] = api_get_js('d3/jquery.xcolor.js');
376
$htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/css/hotspot.css">';
377
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>';
378
$htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_PATH).'build/libs/select2/css/select2.min.css">';
379
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_PATH).'build/libs/select2/js/select2.min.js"></script>';
380
$htmlHeadXtra[] = '<script>$(function(){ if ($.fn.select2){ $(".ch-select2").select2({width:"100%"}); } });</script>';
381
382
$messageMap = [
383
    'ExerciseStored' => 'Exercise stored',
384
    'ItemUpdated' => 'Item updated',
385
    'ItemAdded' => 'Item added',
386
];
387
388
if (isset($_GET['message']) && isset($messageMap[$_GET['message']])) {
389
    Display::addFlash(
390
        Display::return_message(get_lang($messageMap[$_GET['message']]), 'confirmation')
391
    );
392
}
393
394
Display::display_header($nameTools, 'Exercise');
395
396
// If we are in a test
397
$inATest = isset($exerciseId) && $exerciseId > 0;
398
399
if ($inATest) {
400
    $actions = '';
401
    if (isset($_GET['hotspotadmin']) || isset($_GET['newQuestion'])) {
402
        $actions .= '<a
403
        href="'.api_get_path(WEB_CODE_PATH).'exercise/admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
404
            Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Go back to the questions list')).'</a>';
405
    }
406
407
    if (!isset($_GET['hotspotadmin']) && !isset($_GET['newQuestion']) && !isset($_GET['editQuestion'])) {
408
        $actions .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq().'">'.
409
            Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, sprintf(get_lang('Back to %s'), get_lang('Test list'))).'</a>';
410
    }
411
    $actions .= '<a
412
        href="'.api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->getId().'&preview=1">'.
413
        Display::getMdiIcon(ActionIcon::PREVIEW_CONTENT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Preview')).'</a>';
414
415
    $actions .= Display::url(
416
        Display::getMdiIcon('chart-box', 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Results and feedback')),
417
        api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.api_get_cidreq().'&exerciseId='.$objExercise->getId()
418
    );
419
420
    $actions .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise_admin.php?'.api_get_cidreq().'&modifyExercise=yes&exerciseId='.$objExercise->getId().'">'.
421
        Display::getMdiIcon('cog', 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Edit test name and settings')).'</a>';
422
423
    $maxScoreAllQuestions = 0;
424
    if (false === $showPagination) {
425
        $questionList = $objExercise->selectQuestionList(true, $objExercise->random > 0 ? false : true);
426
        if (!empty($questionList)) {
427
            foreach ($questionList as $questionItemId) {
428
                $question = Question::read($questionItemId);
429
                if ($question) {
430
                    $maxScoreAllQuestions += $question->selectWeighting();
431
                }
432
            }
433
        }
434
    }
435
436
    echo Display::toolbarAction('toolbar', [$actions]);
437
438
    if ($objExercise->added_in_lp()) {
439
        echo Display::return_message(
440
            get_lang(
441
                '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.'
442
            ),
443
            'warning'
444
        );
445
    }
446
    if ($editQuestion && $objQuestion->existsInAnotherExercise()) {
447
        echo Display::return_message(
448
            Display::getMdiIcon('alert', 'ch-tool-icon', null, ICON_SIZE_SMALL)
449
            .get_lang('This question is used in another exercises. If you continue its edition, the changes will affect all exercises that contain this question.'),
450
            'warning',
451
            false
452
        );
453
    }
454
455
    $isHotspotEdit = is_object($objQuestion) && in_array((int) $objQuestion->selectType(), [HOT_SPOT, HOT_SPOT_COMBINATION, HOT_SPOT_DELINEATION], true);
456
    $alert = '';
457
    if (false === $showPagination && !$isHotspotEdit) {
458
        $originalSelectionType = $objExercise->questionSelectionType;
459
        $objExercise->questionSelectionType = EX_Q_SELECTION_ORDERED;
460
461
        // Get the full list of question IDs (as configured in this exercise).
462
        /** @var int[] $allIds */
463
        $allIds = (array) $objExercise->selectQuestionList(true, true);
464
465
        // Load questions and build a children map to detect "media" containers reliably.
466
        $questionsById = [];
467
        $childrenByParent = []; // parentId => [childId, ...]
468
        foreach ($allIds as $qid) {
469
            $q = Question::read($qid);
470
            if (!$q) {
471
                continue;
472
            }
473
            $questionsById[$qid] = $q;
474
475
            // some DBs might store parent_id as string/null
476
            $pid = (int) ($q->parent_id ?? 0);
477
            if ($pid > 0) {
478
                if (!isset($childrenByParent[$pid])) {
479
                    $childrenByParent[$pid] = [];
480
                }
481
                $childrenByParent[$pid][] = $qid;
482
            }
483
        }
484
485
        // is this question a media/container?
486
        $isMediaContainer = static function ($q, $qid, $childrenByParent) {
487
            // Case 1: explicit MEDIA_QUESTION type (when constant exists)
488
            $isMediaType = (defined('MEDIA_QUESTION') && (int) $q->selectType() === MEDIA_QUESTION);
489
490
            // Case 2: it is a parent of other questions within this exercise
491
            $isParent = isset($childrenByParent[$qid]) && !empty($childrenByParent[$qid]);
492
493
            // Case 3: some forks expose a method isMedia()
494
            $hasMethod = method_exists($q, 'isMedia') && $q->isMedia();
495
496
            return $isMediaType || $isParent || $hasMethod;
497
        };
498
499
        // Build the effective set of answerable questions (exclude media containers).
500
        $effectiveQuestions = []; // id => Question
501
        foreach ($questionsById as $qid => $q) {
502
            if ($isMediaContainer($q, $qid, $childrenByParent)) {
503
                continue; // skip media/parent containers
504
            }
505
            $effectiveQuestions[$qid] = $q;
506
        }
507
508
        // Compute counts and totals using only effective questions.
509
        $effectiveNbrQuestions = count($effectiveQuestions);
510
        $effectiveTotalScore = 0.0;
511
        foreach ($effectiveQuestions as $q) {
512
            $effectiveTotalScore += (float) $q->selectWeighting();
513
        }
514
515
        // Restore original selection type.
516
        $objExercise->questionSelectionType = $originalSelectionType;
517
518
        // First line: "X questions, total score Y." (media excluded)
519
        $alert .= sprintf(
520
            get_lang('%d questions, for a total score (all questions) of %s.'),
521
            $effectiveNbrQuestions,
522
            $effectiveTotalScore
523
        );
524
525
        // If random selection is enabled, display the limit and an informative max total
526
        if ($objExercise->random > 0) {
527
            $limit = min((int) $objExercise->random, $effectiveNbrQuestions);
528
529
            // Gather weights and take top-N.
530
            $weights = [];
531
            foreach ($effectiveQuestions as $id => $q) {
532
                $weights[$id] = (float) $q->selectWeighting();
533
            }
534
            arsort($weights, SORT_NUMERIC); // highest first
535
536
            $maxScoreSelected = 0.0;
537
            $i = 0;
538
            foreach ($weights as $w) {
539
                $maxScoreSelected += $w;
540
                if (++$i >= $limit) {
541
                    break;
542
                }
543
            }
544
545
            $alert .= '<br />'.sprintf(
546
                    get_lang('Only %s questions will be picked randomly following the quiz configuration.'),
547
                    $limit
548
                );
549
            $alert .= sprintf(
550
                '<br>'.get_lang('Only %d questions will be selected based on the test configuration, for a total score of %s.'),
551
                $limit,
552
                $maxScoreSelected
553
            );
554
        }
555
556
        // Category-based ordered selection: use effective counts/totals as well.
557
        if ($objExercise->questionSelectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED) {
558
            $alert .= sprintf(
559
                '<br>'.get_lang(
560
                    'Only %d questions will be selected based on the test configuration, for a total score of %s.'
561
                ),
562
                $effectiveNbrQuestions,
563
                $effectiveTotalScore
564
            );
565
        }
566
    } else {
567
        // Pagination enabled or hotspot edit: keep a minimal, safe notice for random selection.
568
        if ($objExercise->random > 0) {
569
            $limit = min((int) $objExercise->random, (int) $nbrQuestions);
570
            $alert .= '<br />'.sprintf(
571
                    get_lang('Only %s questions will be picked randomly following the quiz configuration.'),
572
                    $limit
573
                );
574
        }
575
    }
576
    if (!empty($alert)) {
577
        echo Display::return_message($alert, 'normal', false);
578
    }
579
} elseif (isset($_GET['newQuestion'])) {
580
    // we are in create a new question from question pool not in a test
581
    $actions = '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'">'.
582
        Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Go back to the questions list')).'</a>';
583
    echo Display::toolbarAction('toolbar', [$actions]);
584
} else {
585
    // If we are in question_pool but not in a test, go back to the questions created in pool
586
    $actions = '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/question_pool.php?'.api_get_cidreq().'">'.
587
        Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Go back to the questions list')).
588
        '</a>';
589
    echo Display::toolbarAction('toolbar', [$actions]);
590
}
591
592
if ($newQuestion || $editQuestion) {
593
    // Question management
594
    $type = isset($_REQUEST['answerType']) ? Security::remove_XSS($_REQUEST['answerType']) : null;
595
    echo '<input type="hidden" name="Type" value="'.$type.'" />';
596
597
    if ('yes' === $newQuestion) {
598
        $objExercise->edit_exercise_in_lp = true;
599
        require 'question_admin.inc.php';
600
    }
601
    if ($editQuestion) {
602
        // Question preview if teacher clicked the "switch to student"
603
        if ($studentViewActive && $is_allowedToEdit) {
604
            echo '<div class="main-question">';
605
            echo Display::div($objQuestion->selectTitle(), ['class' => 'question_title']);
606
            ExerciseLib::showQuestion(
607
                $objExercise,
608
                $editQuestion,
609
                false,
610
                null,
611
                null,
612
                false,
613
                true,
614
                false,
615
                true,
616
                true
617
            );
618
            echo '</div>';
619
        } else {
620
            require 'question_admin.inc.php';
621
            ExerciseLib::showTestsWhereQuestionIsUsed($objQuestion->iid, $objExercise->getId());
622
        }
623
    }
624
}
625
626
if (isset($_GET['hotspotadmin'])) {
627
    if (!is_object($objQuestion)) {
628
        $objQuestion = Question::read((int) $_GET['hotspotadmin']);
629
    }
630
    if (!$objQuestion) {
631
        api_not_allowed();
632
    }
633
634
    // If we already processed POST above, don't process it again here (avoid double-save).
635
    $hotspot_skip_processing = !empty($hotspotAlreadyProcessed);
636
    $hotspot_skip_display = false;
637
638
    require 'hotspot_admin.inc.php';
639
}
640
641
if (isset($_GET['mad_admin'])) {
642
    $qid = (int) $_GET['mad_admin'];
643
    $objQuestion = Question::read($qid);
644
    if (!$objQuestion) {
0 ignored issues
show
$objQuestion is of type Question, thus it always evaluated to true.
Loading history...
645
        api_not_allowed();
646
    }
647
648
    require 'multiple_answer_dropdown_admin.php';
649
    exit;
650
}
651
652
if (
653
    !$newQuestion
654
    && !$modifyQuestion
655
    && !$editQuestion
656
    && !isset($_GET['hotspotadmin'])
657
    && !isset($_GET['mad_admin'])
658
) {
659
    require 'question_list_admin.inc.php';
660
}
661
662
// if we are in question authoring, display warning to user is feedback not shown at the end of the test -ref #6619
663
// this test to display only message in the question authoring page and not in the question list page too
664
if (EXERCISE_FEEDBACK_TYPE_EXAM == $objExercise->getFeedbackType()) {
665
    echo Display::return_message(
666
        get_lang(
667
            'This test is configured not to display feedback to learners. Comments will not be seen at the end of the test, but may be useful for you, as teacher, when reviewing the question details.'
668
        ),
669
        'normal'
670
    );
671
}
672
673
Session::write('objQuestion', $objQuestion);
674
Session::write('objAnswer', $objAnswer);
675
Display::display_footer();
676