Issues (1798)

public/main/exercise/admin.php (1 issue)

Labels
Severity
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use ChamiloSession as Session;
6
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
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
// Access control
53
api_protect_course_script(true);
54
55
$is_allowedToEdit = api_is_allowed_to_edit(null, true, false, false);
56
$sessionId = api_get_session_id();
57
$studentViewActive = api_is_student_view_active();
58
$showPagination = 'true' === api_get_setting('exercise.show_question_pagination');
59
60
if (!$is_allowedToEdit) {
61
    api_not_allowed(true);
62
}
63
64
$exerciseId = isset($_GET['exerciseId']) ? (int) $_GET['exerciseId'] : 0;
65
$newQuestion = $_GET['newQuestion'] ?? 0;
66
$modifyAnswers = isset($_GET['modifyAnswers']) ? $_GET['modifyAnswers'] : 0;
67
$editQuestion = isset($_GET['editQuestion']) ? $_GET['editQuestion'] : 0;
68
$page = isset($_GET['page']) && !empty($_GET['page']) ? (int) $_GET['page'] : 1;
69
$modifyQuestion = isset($_GET['modifyQuestion']) ? $_GET['modifyQuestion'] : 0;
70
$deleteQuestion = isset($_GET['deleteQuestion']) ? $_GET['deleteQuestion'] : 0;
71
$cloneQuestion = isset($_REQUEST['clone_question']) ? $_REQUEST['clone_question'] : 0;
72
if (empty($questionId)) {
73
    $questionId = Session::read('questionId');
74
}
75
if (empty($modifyExercise)) {
76
    $modifyExercise = isset($_GET['modifyExercise']) ? $_GET['modifyExercise'] : null;
77
}
78
79
$fromExercise = isset($fromExercise) ? $fromExercise : null;
80
$cancelExercise = isset($cancelExercise) ? $cancelExercise : null;
81
$cancelAnswers = isset($cancelAnswers) ? $cancelAnswers : null;
82
$modifyIn = isset($modifyIn) ? $modifyIn : null;
83
$cancelQuestion = isset($cancelQuestion) ? $cancelQuestion : null;
84
85
/* Cleaning all incomplete attempts of the admin/teacher to avoid weird problems
86
    when changing the exercise settings, number of questions, etc */
87
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

87
Event::/** @scrutinizer ignore-call */ 
88
       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...
88
    api_get_user_id(),
89
    $exerciseId,
90
    api_get_course_int_id(),
91
    api_get_session_id()
92
);
93
94
// get from session
95
$objExercise = Session::read('objExercise');
96
$objQuestion = Session::read('objQuestion');
97
98
if (isset($_REQUEST['convertAnswer'])) {
99
    $objQuestion = $objQuestion->swapSimpleAnswerTypes();
100
    Session::write('objQuestion', $objQuestion);
101
}
102
$objAnswer = Session::read('objAnswer');
103
$_course = api_get_course_info();
104
105
// tables used in the exercise tool.
106
if (!empty($_GET['action']) && 'exportqti2' === $_GET['action'] && !empty($_GET['questionId'])) {
107
    require_once 'export/qti2/qti2_export.php';
108
    $export = export_question_qti($_GET['questionId'], true);
109
    $qid = (int) $_GET['questionId'];
110
    $name = 'qti2_export_'.$qid.'.zip';
111
    $zip = api_create_zip($name);
112
    $zip->addFile("qti2export_$qid.xml", $export);
113
    $zip->finish();
114
    exit;
115
}
116
117
// Exercise object creation.
118
if (!($objExercise instanceof Exercise)) {
119
    // creation of a new exercise if wrong or not specified exercise ID
120
    if ($exerciseId) {
121
        $objExercise = new Exercise();
122
        $parseQuestionList = $showPagination > 0 ? false : true;
123
        if ($editQuestion) {
124
            $parseQuestionList = false;
125
            $showPagination = true;
126
        }
127
        $objExercise->read($exerciseId, $parseQuestionList);
128
        Session::write('objExercise', $objExercise);
129
    }
130
}
131
// Exercise can be edited in their course.
132
if (empty($objExercise)) {
133
    Session::erase('objExercise');
134
    header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
135
    exit;
136
}
137
138
// Exercise can be edited in their course.
139
if ($objExercise->sessionId != $sessionId) {
140
    api_not_allowed(true);
141
}
142
143
// doesn't select the exercise ID if we come from the question pool
144
if (!$fromExercise) {
145
    // gets the right exercise ID, and if 0 creates a new exercise
146
    if (!$exerciseId = $objExercise->getId()) {
147
        $modifyExercise = 'yes';
148
    }
149
}
150
151
$nbrQuestions = $objExercise->getQuestionCount();
152
153
// Question object creation.
154
if ($editQuestion || $newQuestion || $modifyQuestion || $modifyAnswers) {
155
    if ($editQuestion || $newQuestion) {
156
        // reads question data
157
        if ($editQuestion) {
158
            // question not found
159
            if (!$objQuestion = Question::read($editQuestion)) {
160
                api_not_allowed(true);
161
            }
162
            // saves the object into the session
163
            Session::write('objQuestion', $objQuestion);
164
        }
165
    }
166
167
    // checks if the object exists
168
    if (is_object($objQuestion)) {
169
        // gets the question ID
170
        $questionId = $objQuestion->getId();
171
    }
172
}
173
174
// if cancelling an exercise
175
if ($cancelExercise) {
176
    // existing exercise
177
    if ($exerciseId) {
178
        unset($modifyExercise);
179
    } else {
180
        // new exercise
181
        // goes back to the exercise list
182
        header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
183
        exit();
184
    }
185
}
186
187
// if cancelling question creation/modification
188
if ($cancelQuestion) {
189
    // if we are creating a new question from the question pool
190
    if (!$exerciseId && !$questionId) {
191
        // goes back to the question pool
192
        header('Location: question_pool.php?'.api_get_cidreq());
193
        exit();
194
    } else {
195
        // goes back to the question viewing
196
        $editQuestion = $modifyQuestion;
197
        unset($newQuestion, $modifyQuestion);
198
    }
199
}
200
201
if (!empty($cloneQuestion) && !empty($objExercise->getId())) {
202
    $oldQuestionObj = Question::read($cloneQuestion);
203
    $oldQuestionObj->question = $oldQuestionObj->question.' - '.get_lang('Copy');
204
205
    $newId = $oldQuestionObj->duplicate(api_get_course_info());
206
    $newQuestionObj = Question::read($newId);
207
    $newQuestionObj->addToList($exerciseId);
208
209
    // Save category to the destination course
210
    if (!empty($oldQuestionObj->category)) {
211
        $newQuestionObj->saveCategory($oldQuestionObj->category);
212
    }
213
214
    // This should be moved to the duplicate function
215
    $newAnswerObj = new Answer($cloneQuestion);
216
    $newAnswerObj->read();
217
    $newAnswerObj->duplicate($newQuestionObj);
218
219
    // Reloading tne $objExercise obj
220
    $objExercise->read($objExercise->getId(), false);
221
222
    Display::addFlash(Display::return_message(get_lang('Item copied')));
223
224
    header('Location: admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->getId().'&page='.$page);
225
    exit;
226
}
227
228
// if cancelling answer creation/modification
229
if ($cancelAnswers) {
230
    // goes back to the question viewing
231
    $editQuestion = $modifyAnswers;
232
    unset($modifyAnswers);
233
}
234
235
$nameTools = '';
236
// modifies the query string that is used in the link of tool name
237
if ($editQuestion || $modifyQuestion || $newQuestion || $modifyAnswers) {
238
    $nameTools = get_lang('Question / Answer management');
239
}
240
241
if (api_is_in_gradebook()) {
242
    $interbreadcrumb[] = [
243
        'url' => Category::getUrl(),
244
        'name' => get_lang('Assessments'),
245
    ];
246
}
247
248
$interbreadcrumb[] = [
249
    'url' => api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq(),
250
    'name' => get_lang('Tests'),
251
];
252
if (isset($_GET['newQuestion']) || isset($_GET['editQuestion'])) {
253
    $interbreadcrumb[] = [
254
        'url' => api_get_path(WEB_CODE_PATH).'exercise/admin.php?exerciseId='.$objExercise->getId().'&'.api_get_cidreq(),
255
        'name' => $objExercise->selectTitle(true),
256
    ];
257
} else {
258
    $interbreadcrumb[] = [
259
        'url' => '#',
260
        'name' => $objExercise->selectTitle(true),
261
    ];
262
}
263
264
// shows a link to go back to the question pool
265
if (!$exerciseId && $nameTools != get_lang('Tests management')) {
266
    $interbreadcrumb[] = [
267
        'url' => api_get_path(WEB_CODE_PATH)."exercise/question_pool.php?fromExercise=$fromExercise&".api_get_cidreq(),
268
        'name' => get_lang('Recycle existing questions'),
269
    ];
270
}
271
272
// if the question is duplicated, disable the link of tool name
273
if ('thisExercise' === $modifyIn) {
274
    if ($buttonBack) {
275
        $modifyIn = 'allExercises';
276
    }
277
}
278
279
$htmlHeadXtra[] = api_get_build_js('legacy_exercise.js');
280
281
$template = new Template();
282
$templateName = $template->get_template('exercise/submit.js.tpl');
283
$htmlHeadXtra[] = $template->fetch($templateName);
284
$htmlHeadXtra[] = api_get_js('d3/jquery.xcolor.js');
285
$htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/css/hotspot.css">';
286
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>';
287
288
if (isset($_GET['message'])) {
289
    if (in_array($_GET['message'], ['ExerciseStored', 'ItemUpdated', 'ItemAdded'])) {
290
        Display::addFlash(Display::return_message(get_lang($_GET['message']), 'confirmation'));
291
    }
292
}
293
294
Display::display_header($nameTools, 'Exercise');
295
296
// If we are in a test
297
$inATest = isset($exerciseId) && $exerciseId > 0;
298
299
if ($inATest) {
300
    $actions = '';
301
    if (isset($_GET['hotspotadmin']) || isset($_GET['newQuestion'])) {
302
        $actions .= '<a
303
        href="'.api_get_path(WEB_CODE_PATH).'exercise/admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
304
            Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Go back to the questions list')).'</a>';
305
    }
306
307
    if (!isset($_GET['hotspotadmin']) && !isset($_GET['newQuestion']) && !isset($_GET['editQuestion'])) {
308
        $actions .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq().'">'.
309
            Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Back to tests list')).'</a>';
310
    }
311
    $actions .= '<a
312
        href="'.api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->getId().'&preview=1">'.
313
        Display::getMdiIcon(ActionIcon::PREVIEW_CONTENT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Preview')).'</a>';
314
315
    $actions .= Display::url(
316
        Display::getMdiIcon('chart-box', 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Results and feedback')),
317
        api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.api_get_cidreq().'&exerciseId='.$objExercise->getId()
318
    );
319
320
    $actions .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise_admin.php?'.api_get_cidreq().'&modifyExercise=yes&exerciseId='.$objExercise->getId().'">'.
321
        Display::getMdiIcon('cog', 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Edit test name and settings')).'</a>';
322
323
    $maxScoreAllQuestions = 0;
324
    if (false === $showPagination) {
325
        $questionList = $objExercise->selectQuestionList(true, $objExercise->random > 0 ? false : true);
326
        if (!empty($questionList)) {
327
            foreach ($questionList as $questionItemId) {
328
                $question = Question::read($questionItemId);
329
                if ($question) {
330
                    $maxScoreAllQuestions += $question->selectWeighting();
331
                }
332
            }
333
        }
334
    }
335
336
    echo Display::toolbarAction('toolbar', [$actions]);
337
338
    if ($objExercise->added_in_lp()) {
339
        echo Display::return_message(
340
            get_lang(
341
                '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.'
342
            ),
343
            'warning'
344
        );
345
    }
346
    if ($editQuestion && $objQuestion->existsInAnotherExercise()) {
347
        echo Display::return_message(
348
            Display::getMdiIcon('alert', 'ch-tool-icon', null, ICON_SIZE_SMALL)
349
                .get_lang('Warning: This question exists in another tests'),
350
            'warning',
351
            false
352
        );
353
    }
354
355
    $alert = '';
356
    if (false === $showPagination) {
357
        $originalSelectionType = $objExercise->questionSelectionType;
358
        $objExercise->questionSelectionType = EX_Q_SELECTION_ORDERED;
359
360
        $outMaxScore = 0;
361
        $outMaxScore = array_reduce(
362
            $objExercise->selectQuestionList(true, true),
363
            function ($acc, $questionId) {
364
                $objQuestionTmp = Question::read($questionId);
365
366
                return $acc + $objQuestionTmp->selectWeighting();
367
            },
368
            0
369
        );
370
371
        $objExercise->questionSelectionType = $originalSelectionType;
372
        $alert .= sprintf(
373
            get_lang('%d questions, for a total score (all questions) of %s.'),
374
            $nbrQuestions,
375
            $outMaxScore
376
        );
377
    }
378
    if ($objExercise->random > 0) {
379
        $alert .= '<br />'.sprintf(get_lang('OnlyXQuestionsPickedRandomly'), $objExercise->random);
380
        $alert .= sprintf(
381
            '<br>'.get_lang('XQuestionsSelectedWithTotalScoreY'),
382
            $objExercise->random,
383
            $maxScoreAllQuestions
384
        );
385
    }
386
    if ($objExercise->random > 0) {
387
        $alert .= '<br />'.sprintf(get_lang('OnlyXQuestionsPickedRandomly'), $objExercise->random);
388
        $alert .= sprintf(
389
            '<br>'.get_lang('XQuestionsSelectedWithTotalScoreY'),
390
            $objExercise->random,
391
            $maxScoreAllQuestions
392
        );
393
    }
394
    if (false === $showPagination) {
395
        if ($objExercise->questionSelectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED) {
396
            $alert .= sprintf(
397
                '<br>'.get_lang(
398
                    'Only %d questions will be selected based on the test configuration, for a total score of %s.'
399
                ),
400
                count($questionList),
401
                $maxScoreAllQuestions
402
            );
403
        }
404
    }
405
    echo Display::return_message($alert, 'normal', false);
406
} elseif (isset($_GET['newQuestion'])) {
407
    // we are in create a new question from question pool not in a test
408
    $actions = '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'">'.
409
        Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Go back to the questions list')).'</a>';
410
    echo Display::toolbarAction('toolbar', [$actions]);
411
} else {
412
    // If we are in question_pool but not in a test, go back to the questions created in pool
413
    $actions = '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/question_pool.php?'.api_get_cidreq().'">'.
414
        Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Go back to the questions list')).
415
        '</a>';
416
    echo Display::toolbarAction('toolbar', [$actions]);
417
}
418
419
if ($newQuestion || $editQuestion) {
420
    // Question management
421
    $type = isset($_REQUEST['answerType']) ? Security::remove_XSS($_REQUEST['answerType']) : null;
422
    echo '<input type="hidden" name="Type" value="'.$type.'" />';
423
424
    if ('yes' === $newQuestion) {
425
        $objExercise->edit_exercise_in_lp = true;
426
        require 'question_admin.inc.php';
427
    }
428
    if ($editQuestion) {
429
        // Question preview if teacher clicked the "switch to student"
430
        if ($studentViewActive && $is_allowedToEdit) {
431
            echo '<div class="main-question">';
432
            echo Display::div($objQuestion->selectTitle(), ['class' => 'question_title']);
433
            ExerciseLib::showQuestion(
434
                $objExercise,
435
                $editQuestion,
436
                false,
437
                null,
438
                null,
439
                false,
440
                true,
441
                false,
442
                true,
443
                true
444
            );
445
            echo '</div>';
446
        } else {
447
            require 'question_admin.inc.php';
448
            ExerciseLib::showTestsWhereQuestionIsUsed($objQuestion->iid, $objExercise->getId());
449
        }
450
    }
451
}
452
453
if (isset($_GET['hotspotadmin'])) {
454
    if (!is_object($objQuestion)) {
455
        $objQuestion = Question::read($_GET['hotspotadmin']);
456
    }
457
    if (!$objQuestion) {
458
        api_not_allowed();
459
    }
460
    require 'hotspot_admin.inc.php';
461
}
462
463
if (!$newQuestion && !$modifyQuestion && !$editQuestion && !isset($_GET['hotspotadmin'])) {
464
    // question list management
465
    require 'question_list_admin.inc.php';
466
}
467
468
// if we are in question authoring, display warning to user is feedback not shown at the end of the test -ref #6619
469
// this test to display only message in the question authoring page and not in the question list page too
470
if (EXERCISE_FEEDBACK_TYPE_EXAM == $objExercise->getFeedbackType()) {
471
    echo Display::return_message(
472
        get_lang(
473
            '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.'
474
        ),
475
        'normal'
476
    );
477
}
478
479
Session::write('objQuestion', $objQuestion);
480
Session::write('objAnswer', $objAnswer);
481
Display::display_footer();
482