Passed
Push — master ( e8a665...89fe32 )
by
unknown
17:54 queued 08:52
created

get_action_icon_for_question()   C

Complexity

Conditions 11
Paths 9

Size

Total Lines 90
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 56
nc 9
nop 12
dl 0
loc 90
rs 6.8133
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/* For licensing terms, see /license.txt */
3
4
use Chamilo\CoreBundle\Entity\ExtraField as ExtraFieldEntity;
5
use Chamilo\CoreBundle\Enums\ActionIcon;
6
use Chamilo\CoreBundle\Enums\ObjectIcon;
7
use Chamilo\CoreBundle\Framework\Container;
8
use Chamilo\CourseBundle\Entity\CQuizQuestion;
9
use Chamilo\CourseBundle\Entity\CQuizRelQuestion;
10
use ChamiloSession as Session;
11
use Doctrine\ORM\NoResultException;
12
use Doctrine\ORM\QueryBuilder;
13
use Knp\Component\Pager\Paginator;
14
15
/**
16
 * Question Pool
17
 * This script allows administrators to manage questions and add them into their exercises.
18
 * One question can be in several exercises.
19
 *
20
 * @author Olivier Brouckaert
21
 * @author Julio Montoya adding support to query all questions from all session, courses, exercises
22
 * @author Modify by hubert borderiou 2011-10-21 Question's category
23
 */
24
require_once __DIR__.'/../inc/global.inc.php';
25
26
api_protect_course_script(true);
27
28
$this_section = SECTION_COURSES;
29
$is_allowedToEdit = api_is_allowed_to_edit(null, true);
30
31
$delete = isset($_GET['delete']) ? (int) $_GET['delete'] : null;
32
$recup = isset($_GET['recup']) ? (int) $_GET['recup'] : null;
33
$fromExercise = isset($_REQUEST['fromExercise']) ? (int) $_REQUEST['fromExercise'] : null;
34
$exerciseId = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : null;
35
$courseCategoryId = isset($_REQUEST['courseCategoryId']) ? (int) $_REQUEST['courseCategoryId'] : null;
36
$exerciseLevel = isset($_REQUEST['exerciseLevel']) ? (int) $_REQUEST['exerciseLevel'] : -1;
37
$answerType = isset($_REQUEST['answerType']) ? (int) $_REQUEST['answerType'] : null;
38
$question_copy = isset($_REQUEST['question_copy']) ? (int) $_REQUEST['question_copy'] : 0;
39
$session_id = isset($_REQUEST['session_id']) ? (int) $_REQUEST['session_id'] : null;
40
$selected_course = isset($_GET['selected_course']) ? (int) $_GET['selected_course'] : null;
41
42
// save the id of the previous course selected by user to reset menu if we detect that user change course hub 13-10-2011
43
$course_id_changed = isset($_GET['course_id_changed']) ? (int) $_GET['course_id_changed'] : null;
44
// save the id of the previous exercise selected by user to reset menu if we detect that user change course hub 13-10-2011
45
$exercise_id_changed = isset($_GET['exercise_id_changed']) ? (int) $_GET['exercise_id_changed'] : null;
46
47
$questionId = isset($_GET['question_id']) && !empty($_GET['question_id']) ? (int) $_GET['question_id'] : '';
48
$description = isset($_GET['description']) ? Database::escape_string($_GET['description']) : '';
49
$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
50
51
// By default when we go to the page for the first time, we select the current course.
52
// NOTE: legacy behavior keeps -1 as "Select", so we normalize inside getQuestions().
53
if (!isset($_GET['selected_course']) && !isset($_GET['exerciseId'])) {
54
    $selected_course = -1;
55
}
56
57
$_course = api_get_course_info();
58
$objExercise = new Exercise();
59
if (!empty($fromExercise)) {
60
    $objExercise->read($fromExercise, false);
61
}
62
63
$nameTools = get_lang('Recycle existing questions');
64
$interbreadcrumb[] = ['url' => 'exercise.php?'.api_get_cidreq(), 'name' => get_lang('Tests')];
65
66
if (!empty($objExercise->id)) {
67
    $interbreadcrumb[] = [
68
        'url' => 'admin.php?exerciseId='.$objExercise->id.'&'.api_get_cidreq(),
69
        'name' => $objExercise->selectTitle(true),
70
    ];
71
}
72
73
// message to be displayed if actions successful
74
$displayMessage = '';
75
if ($is_allowedToEdit) {
76
    // Duplicating a Question
77
    if (!isset($_POST['recup']) && 0 != $question_copy && isset($fromExercise)) {
78
        $origin_course_id = (int) $_GET['course_id'];
79
        $origin_course_info = api_get_course_info_by_id($origin_course_id);
80
        $current_course = api_get_course_info();
81
        $old_question_id = $question_copy;
82
        // Reading the source question
83
        $old_question_obj = Question::read($old_question_id, $origin_course_info);
84
        $courseId = $current_course['real_id'];
85
        if ($old_question_obj) {
0 ignored issues
show
introduced by
$old_question_obj is of type Question, thus it always evaluated to true.
Loading history...
86
            $old_question_obj->updateTitle($old_question_obj->selectTitle().' - '.get_lang('Copy'));
87
            //Duplicating the source question, in the current course
88
            $new_id = $old_question_obj->duplicate($current_course);
89
            //Reading new question
90
            $new_question_obj = Question::read($new_id);
91
            $new_question_obj->addToList($fromExercise, true);
92
            //Reading Answers obj of the current course
93
            $new_answer_obj = new Answer($old_question_id, $origin_course_id);
94
            $new_answer_obj->read();
95
            //Duplicating the Answers in the current course
96
            $new_answer_obj->duplicate($new_question_obj, $current_course);
97
            // destruction of the Question object
98
            unset($new_question_obj);
99
            unset($old_question_obj);
100
101
            $objExercise = new Exercise($courseId);
102
            $objExercise->read($fromExercise);
103
            Session::write('objExercise', $objExercise);
104
        }
105
        $displayMessage = get_lang('Item added');
106
    }
107
108
    // Deletes a question from the database and all exercises
109
    if ($delete) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $delete of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
110
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
111
        if ($limitTeacherAccess && !api_is_platform_admin()) {
112
            api_not_allowed(true);
113
        }
114
        // Construction of the Question object
115
        $objQuestionTmp = isQuestionInActiveQuiz($delete) ? false : Question::read($delete);
116
        // if the question exists
117
        if ($objQuestionTmp) {
118
            // deletes the question from all exercises
119
            $objQuestionTmp->delete();
120
121
            // solving the error that when deleting a question from the question pool it is not displaying all questions
122
            $exerciseId = null;
123
        }
124
        // destruction of the Question object
125
        unset($objQuestionTmp);
126
    } elseif ($recup && $fromExercise) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fromExercise of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $recup of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
127
        // gets an existing question and copies it into a new exercise
128
        $objQuestionTmp = Question::read($recup);
129
        // if the question exists
130
        if ($objQuestionTmp) {
0 ignored issues
show
introduced by
$objQuestionTmp is of type Question, thus it always evaluated to true.
Loading history...
131
            /* Adds the exercise ID represented by $fromExercise into the list
132
            of exercises for the current question */
133
            $objQuestionTmp->addToList($fromExercise);
134
        }
135
        // destruction of the Question object
136
        unset($objQuestionTmp);
137
138
        if (!$objExercise instanceof Exercise) {
0 ignored issues
show
introduced by
$objExercise is always a sub-type of Exercise.
Loading history...
139
            $objExercise = new Exercise();
140
            $objExercise->read($fromExercise);
141
        }
142
        // Adds the question ID represented by $recup into the list of questions for the current exercise
143
        $objExercise->addToList($recup);
144
        Session::write('objExercise', $objExercise);
145
        Display::addFlash(Display::return_message(get_lang('Item added'), 'success'));
146
    } elseif (isset($_POST['recup']) && is_array($_POST['recup']) && $fromExercise) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fromExercise of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
147
        $list_recup = $_POST['recup'];
148
        foreach ($list_recup as $course_id => $question_data) {
149
            $origin_course_id = (int) $course_id;
150
            $origin_course_info = api_get_course_info_by_id($origin_course_id);
151
            $current_course = api_get_course_info();
152
            foreach ($question_data as $old_question_id) {
153
                // Reading the source question
154
                $old_question_obj = Question::read($old_question_id, $origin_course_info);
155
                if ($old_question_obj) {
156
                    $old_question_obj->updateTitle(
157
                        $old_question_obj->selectTitle().' - '.get_lang('Copy')
158
                    );
159
160
                    // Duplicating the source question, in the current course
161
                    $new_id = $old_question_obj->duplicate($current_course);
162
163
                    // Reading new question
164
                    $new_question_obj = Question::read($new_id);
165
                    $new_question_obj->addToList($fromExercise);
166
167
                    //Reading Answers obj of the current course
168
                    $new_answer_obj = new Answer($old_question_id, $origin_course_id);
169
                    $new_answer_obj->read();
170
171
                    //Duplicating the Answers in the current course
172
                    $new_answer_obj->duplicate($new_question_obj, $current_course);
173
174
                    // destruction of the Question object
175
                    unset($new_question_obj);
176
                    unset($old_question_obj);
177
178
                    if (!$objExercise instanceof Exercise) {
179
                        $objExercise = new Exercise();
180
                        $objExercise->read($fromExercise);
181
                    }
182
                }
183
            }
184
        }
185
        Session::write('objExercise', $objExercise);
186
    }
187
}
188
189
if (api_is_in_gradebook()) {
190
    $interbreadcrumb[] = [
191
        'url' => Category::getUrl(),
192
        'name' => get_lang('Assessments'),
193
    ];
194
}
195
196
// if admin of course
197
if (!$is_allowedToEdit) {
198
    api_not_allowed(true);
199
}
200
201
$confirmYourChoice = addslashes(api_htmlentities(get_lang('Please confirm your choice'), ENT_QUOTES));
202
$htmlHeadXtra[] = "
203
<script>
204
    document.addEventListener('DOMContentLoaded', function() {
205
      var actionButton = document.querySelector('.action-button');
206
      var dropdownMenu = document.getElementById('action-dropdown');
207
208
      function toggleDropdown(event) {
209
        event.preventDefault();
210
        var isDisplayed = dropdownMenu && dropdownMenu.style.display === 'block';
211
        if (dropdownMenu) dropdownMenu.style.display = isDisplayed ? 'none' : 'block';
212
      }
213
214
      if (actionButton) {
215
        actionButton.addEventListener('click', toggleDropdown);
216
        document.addEventListener('click', function(event) {
217
          if (dropdownMenu && !dropdownMenu.contains(event.target) && !actionButton.contains(event.target)) {
218
            dropdownMenu.style.display = 'none';
219
          }
220
        });
221
      }
222
    });
223
224
    function submit_form(obj) {
225
        document.question_pool.submit();
226
    }
227
228
    function mark_course_id_changed() {
229
        $('#course_id_changed').val('1');
230
    }
231
232
    function mark_exercise_id_changed() {
233
        $('#exercise_id_changed').val('1');
234
    }
235
236
    function confirm_your_choice() {
237
        return confirm('$confirmYourChoice');
238
    }
239
</script>";
240
241
$url = api_get_self().'?'.api_get_cidreq().'&'.http_build_query(
242
        [
243
            'fromExercise' => $fromExercise,
244
            'session_id' => $session_id,
245
            'selected_course' => $selected_course,
246
            'courseCategoryId' => $courseCategoryId,
247
            'exerciseId' => $exerciseId,
248
            'exerciseLevel' => $exerciseLevel,
249
            'answerType' => $answerType,
250
            'question_id' => $questionId,
251
            'description' => Security::remove_XSS($description),
252
            'course_id_changed' => $course_id_changed,
253
            'exercise_id_changed' => $exercise_id_changed,
254
        ]
255
    );
256
257
if (isset($_REQUEST['action'])) {
258
    switch ($_REQUEST['action']) {
259
        case 'reuse':
260
            if (!empty($_REQUEST['questions']) && !empty($fromExercise)) {
261
                $questions = $_REQUEST['questions'];
262
                $objExercise = new Exercise();
263
                $objExercise->read($fromExercise, false);
264
265
                if (count($questions) > 0) {
266
                    foreach ($questions as $questionId) {
267
                        // gets an existing question and copies it into a new exercise
268
                        $objQuestionTmp = Question::read($questionId);
269
                        // if the question exists
270
                        if ($objQuestionTmp) {
271
                            if (false === $objExercise->hasQuestion($questionId)) {
272
                                $objExercise->addToList($questionId);
273
                                $objQuestionTmp->addToList($fromExercise);
274
                            }
275
                        }
276
                    }
277
                }
278
279
                Display::addFlash(Display::return_message(get_lang('Added')));
280
                header('Location: '.$url);
281
                exit;
282
            }
283
284
            break;
285
286
        case 'clone':
287
            if (!empty($_REQUEST['questions']) && !empty($fromExercise)) {
288
                $questions = $_REQUEST['questions'];
289
                $origin_course_id = (int) $_GET['course_id'];
290
291
                $origin_course_info = api_get_course_info_by_id($origin_course_id);
292
                $current_course = api_get_course_info();
293
294
                if (count($questions) > 0) {
295
                    foreach ($questions as $questionId) {
296
                        // Reading the source question
297
                        $old_question_obj = Question::read($questionId, $origin_course_info);
298
                        if ($old_question_obj) {
299
                            $old_question_obj->updateTitle($old_question_obj->selectTitle().' - '.get_lang('Copy'));
300
                            // Duplicating the source question, in the current course
301
                            $new_id = $old_question_obj->duplicate($current_course);
302
                            // Reading new question
303
                            $new_question_obj = Question::read($new_id);
304
                            $new_question_obj->addToList($fromExercise);
305
                            //Reading Answers obj of the current course
306
                            $new_answer_obj = new Answer($questionId, $origin_course_id);
307
                            $new_answer_obj->read();
308
                            //Duplicating the Answers in the current course
309
                            $new_answer_obj->duplicate($new_question_obj, $current_course);
310
                            // destruction of the Question object
311
                            unset($new_question_obj);
312
                            unset($old_question_obj);
313
                        }
314
                    }
315
                }
316
317
                Display::addFlash(Display::return_message(get_lang('Added')));
318
                header('Location: '.$url);
319
                exit;
320
            }
321
322
            break;
323
    }
324
}
325
326
// Form
327
$sessionList = SessionManager::get_sessions_by_user(api_get_user_id(), api_is_platform_admin());
328
$session_select_list = ['-1' => get_lang('Select')];
329
foreach ($sessionList as $item) {
330
    $session_select_list[$item['session_id']] = $item['session_name'];
331
}
332
333
// Course list, get course list of session, or for course where user is admin
334
$course_list = [];
335
336
if (!empty($session_id) && '-1' != $session_id && !empty($sessionList)) {
337
    $sessionInfo = [];
338
    foreach ($sessionList as $session) {
339
        if ($session['session_id'] == $session_id) {
340
            $sessionInfo = $session;
341
        }
342
    }
343
    $course_list = $sessionInfo['courses'];
344
} else {
345
    if (api_is_platform_admin()) {
346
        $course_list = CourseManager::get_courses_list(0, 0, 'title');
347
    } else {
348
        $course_list = CourseManager::get_course_list_of_user_as_course_admin(api_get_user_id());
349
    }
350
351
    // Admin fix, add the current course in the question pool.
352
    if (api_is_platform_admin()) {
353
        $courseInfo = api_get_course_info();
354
        if (!empty($course_list)) {
355
            if (!in_array($courseInfo['real_id'], $course_list)) {
356
                $course_list = array_merge($course_list, [$courseInfo]);
357
            }
358
        } else {
359
            $course_list = [$courseInfo];
360
        }
361
    }
362
}
363
364
$course_select_list = ['-1' => get_lang('Select')];
365
foreach ($course_list as $item) {
366
    $courseItemId = $item['real_id'];
367
    $courseInfo = api_get_course_info_by_id($courseItemId);
368
    $course_select_list[$courseItemId] = '';
369
    if ($courseItemId == api_get_course_int_id()) {
370
        $course_select_list[$courseItemId] = '>&nbsp;&nbsp;&nbsp;&nbsp;';
371
    }
372
    $course_select_list[$courseItemId] .= $courseInfo['title'];
373
}
374
375
if (empty($selected_course) || '-1' == $selected_course) {
376
    $course_info = api_get_course_info();
377
    // no course selected, reset menu test / difficulty / answer type
378
    reset_menu_exo_lvl_type();
379
} else {
380
    $course_info = api_get_course_info_by_id($selected_course);
381
}
382
383
// If course has changed, reset the menu default
384
if ($course_id_changed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $course_id_changed of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
385
    reset_menu_exo_lvl_type();
386
}
387
388
// Get category list for the course $selected_course
389
$categoryList = TestCategory::getCategoriesIdAndName($selected_course);
390
391
// Get exercise list for this course
392
$exercise_list = ExerciseLib::get_all_exercises_for_course_id(
393
    $selected_course,
394
    (empty($session_id) ? 0 : $session_id),
395
    false
396
);
397
398
if (1 == $exercise_id_changed) {
399
    reset_menu_lvl_type();
400
}
401
402
// Exercise List
403
$my_exercise_list = [];
404
$my_exercise_list['0'] = get_lang('All tests');
405
$my_exercise_list['-1'] = get_lang('Orphan questions');
406
$titleSavedAsHtml = ('true' === api_get_setting('editor.save_titles_as_html'));
407
if (is_array($exercise_list)) {
408
    foreach ($exercise_list as $row) {
409
        $my_exercise_list[$row['iid']] = '';
410
        if ($row['iid'] == $fromExercise && $selected_course == api_get_course_int_id()) {
411
            $my_exercise_list[$row['iid']] = '>&nbsp;&nbsp;&nbsp;&nbsp;';
412
        }
413
414
        $exerciseTitle = $row['title'];
415
        if ($titleSavedAsHtml) {
416
            $exerciseTitle = strip_tags(api_html_entity_decode(trim($exerciseTitle)));
417
        }
418
        $my_exercise_list[$row['iid']] .= $exerciseTitle;
419
    }
420
}
421
422
// Difficulty list (only from 0 to 5)
423
$levels = [
424
    -1 => get_lang('All'),
425
    0 => 0,
426
    1 => 1,
427
    2 => 2,
428
    3 => 3,
429
    4 => 4,
430
    5 => 5,
431
];
432
433
// Answer type
434
$question_list = Question::getQuestionTypeList();
435
436
$new_question_list = [];
437
$new_question_list['-1'] = get_lang('All');
438
if (!empty($_course)) {
439
    $feedbackType = $objExercise->getFeedbackType();
440
441
    if (in_array($feedbackType, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
442
        // Keep original base: only these two were allowed before.
443
        $allowedTypes = [
444
            UNIQUE_ANSWER        => true,
445
            HOT_SPOT_DELINEATION => true,
446
        ];
447
448
        // Start from all known types.
449
        $allTypes = $question_list;
450
451
        // Exclude open question types (no immediate feedback).
452
        unset($allTypes[FREE_ANSWER]);
453
        unset($allTypes[ORAL_EXPRESSION]);
454
        unset($allTypes[ANNOTATION]);
455
        unset($allTypes[MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY]);
456
        unset($allTypes[UPLOAD_ANSWER]);
457
        unset($allTypes[ANSWER_IN_OFFICE_DOC]);
458
        unset($allTypes[PAGE_BREAK]);
459
460
        // Append remaining non-open types (do not override base ones).
461
        foreach ($allTypes as $key => $item) {
462
            if (!isset($allowedTypes[$key])) {
463
                $allowedTypes[$key] = true;
464
            }
465
        }
466
467
        // Build the final select list in a stable order.
468
        foreach ($allowedTypes as $key => $_) {
469
            if (isset($question_list[$key])) {
470
                $item = $question_list[$key];
471
                $labelKey = $item[2] ?? $item[1];
472
                $new_question_list[$key] = get_lang($labelKey);
473
            }
474
        }
475
    } else {
476
        // Default behavior for non-adaptative / other feedback types:
477
        // keep all question types except HOT_SPOT_DELINEATION.
478
        foreach ($question_list as $key => $item) {
479
            if (HOT_SPOT_DELINEATION == $key) {
480
                continue;
481
            }
482
            $labelKey = $item[2] ?? $item[1];
483
            $new_question_list[$key] = get_lang($labelKey);
484
        }
485
    }
486
}
487
488
// Answer type list
489
$form = new FormValidator('question_pool', 'GET', $url);
490
$form->addHidden('cidReq', $_course['real_id']);
491
$form->addHidden('cid', api_get_course_int_id());
492
$form->addHidden('sid', api_get_session_id());
493
$form->addHidden('fromExercise', $fromExercise);
494
495
$form
496
    ->addSelect(
497
        'session_id',
498
        get_lang('Session'),
499
        $session_select_list,
500
        ['onchange' => 'submit_form(this)', 'id' => 'session_id']
501
    )
502
    ->setSelected($session_id);
503
504
$form
505
    ->addSelect(
506
        'selected_course',
507
        get_lang('Course'),
508
        $course_select_list,
509
        ['onchange' => 'mark_course_id_changed(); submit_form(this);', 'id' => 'selected_course']
510
    )
511
    ->setSelected($selected_course);
512
513
$form
514
    ->addSelect(
515
        'courseCategoryId',
516
        get_lang('Questions category'),
517
        $categoryList,
518
        ['onchange' => 'submit_form(this);', 'id' => 'courseCategoryId']
519
    )
520
    ->setSelected($courseCategoryId);
521
522
$form
523
    ->addSelect(
524
        'exerciseId',
525
        get_lang('Test'),
526
        $my_exercise_list,
527
        ['onchange' => 'mark_exercise_id_changed(); submit_form(this);', 'id' => 'exerciseId']
528
    )
529
    ->setSelected($exerciseId);
530
531
$form
532
    ->addSelect(
533
        'exerciseLevel',
534
        get_lang('Difficulty'),
535
        $levels,
536
        ['onchange' => 'submit_form(this);', 'id' => 'exerciseLevel']
537
    )
538
    ->setSelected($exerciseLevel);
539
540
$form
541
    ->addSelect(
542
        'answerType',
543
        get_lang('Answer type'),
544
        $new_question_list,
545
        ['onchange' => 'submit_form(this);', 'id' => 'answerType']
546
    )
547
    ->setSelected($answerType);
548
549
$form
550
    ->addText('question_id', get_lang('Id'), false)
551
    ->setValue($questionId);
552
553
$form
554
    ->addText('description', get_lang('Description'), false)
555
    ->setValue(Security::remove_XSS($description));
556
557
$form->addHidden('course_id_changed', '0');
558
$form->addHidden('exercise_id_changed', '0');
559
560
$extraField = new ExtraField('question');
561
$jsForExtraFields = $extraField->addElements($form, 0, [], true);
562
563
$form->addButtonFilter(get_lang('Filter'), 'name');
564
565
if (isset($fromExercise) && $fromExercise > 0) {
566
    $titleAdd = get_lang('Add question to test');
567
} else {
568
    $titleAdd = get_lang('Manage all questions');
569
}
570
571
$form->addHeader($nameTools.' - '.$titleAdd);
572
573
/**
574
 * @return array
575
 */
576
function getExtraFieldConditions(array $formValues, $queryType = 'from')
577
{
578
    $extraField = new ExtraField('question');
579
    $fields = $extraField->get_all(
580
        ['visible_to_self = ? AND filter = ?' => [1, 1]],
581
        'display_text'
582
    );
583
584
    $from = '';
585
    $where = '';
586
587
    foreach ($fields as $field) {
588
        $variable = $field['variable'];
589
590
        if (empty($formValues["extra_$variable"])) {
591
            continue;
592
        }
593
594
        $value = $formValues["extra_$variable"];
595
596
        switch ($field['value_type']) {
597
            case ExtraField::FIELD_TYPE_CHECKBOX:
598
                $value = $value["extra_$variable"];
599
                break;
600
            case ExtraField::FIELD_TYPE_DOUBLE_SELECT:
601
                if (!isset($value["extra_{$variable}_second"])) {
602
                    $value = null;
603
                    break;
604
                }
605
606
                $value = $value["extra_$variable"].'::'.$value["extra_{$variable}_second"];
607
                break;
608
        }
609
610
        if (empty($value)) {
611
            continue;
612
        }
613
614
        if ('from' === $queryType) {
615
            $from .= ", extra_field_values efv_$variable, extra_field ef_$variable";
616
            $where .= "AND (
617
                    qu.iid = efv_$variable.item_id
618
                    AND efv_$variable.field_id = ef_$variable.id
619
                    AND ef_$variable.item_type = ".ExtraFieldEntity::QUESTION_FIELD_TYPE."
620
                    AND ef_$variable.variable = '$variable'
621
                    AND efv_$variable.field_value = '$value'
622
                )";
623
        } elseif ('join' === $queryType) {
624
            $from .= " INNER JOIN extra_field_values efv_$variable ON qu.iid = efv_$variable.item_id
625
                INNER JOIN extra_field ef_$variable ON efv_$variable.field_id = ef_$variable.id";
626
            $where .= "AND (
627
                    ef_$variable.item_type = ".ExtraFieldEntity::QUESTION_FIELD_TYPE."
628
                    AND ef_$variable.variable = '$variable'
629
                    AND efv_$variable.field_value = '$value'
630
                )";
631
        }
632
    }
633
634
    return [
635
        'from' => $from,
636
        'where' => $where,
637
    ];
638
}
639
640
/**
641
 * Apply "active" constraints on a ResourceLink alias.
642
 *
643
 * This mirrors the UI/ExerciseLib behavior:
644
 * - Ignore soft-deleted links
645
 * - Ignore ended links (endVisibilityAt IS NULL)
646
 * - Keep only standard visibilities (0 or 2)
647
 * - When a session is selected, include both course-level links (session IS NULL)
648
 *   and session links (session = :sessionId)
649
 */
650
function applyActiveResourceLinkConstraints(QueryBuilder $qb, string $alias, int $sessionId, bool $includeCourseWhenSessionSelected = true): void
651
{
652
    $qb->andWhere($alias.'.deletedAt IS NULL');
653
    $qb->andWhere($alias.'.endVisibilityAt IS NULL');
654
    $qb->andWhere($alias.'.visibility IN (0,2)');
655
656
    if ($sessionId > 0) {
657
        if ($includeCourseWhenSessionSelected) {
658
            $qb->andWhere('(IDENTITY('.$alias.'.session) = :sessionId OR '.$alias.'.session IS NULL)');
659
        } else {
660
            $qb->andWhere('IDENTITY('.$alias.'.session) = :sessionId');
661
        }
662
    } else {
663
        $qb->andWhere($alias.'.session IS NULL');
664
    }
665
}
666
667
/**
668
 * Fetch questions using Doctrine (C2).
669
 *
670
 * Important UI rule:
671
 * - We do NOT exclude questions already linked to the current quiz (fromExercise).
672
 *   The UI already disables checkboxes/actions through $objExercise->hasQuestion().
673
 *
674
 * Soft delete rule:
675
 * - A question is considered linked to a quiz ONLY if that quiz is still "active"
676
 *   in this context (ResourceLink.deletedAt IS NULL).
677
 */
678
function getQuestions(
679
    $getCount,
680
    $start,
681
    $length,
682
    $exerciseId,
683
    $courseCategoryId,
684
    $selectedCourse,
685
    $sessionId,
686
    $exerciseLevel,
687
    $answerType,
688
    $questionId,
689
    $description,
690
    $fromExercise = 0,
691
    $formValues = []
692
) {
693
    $entityManager = Database::getManager();
694
695
    // Normalize inputs
696
    $selectedCourse = (int) $selectedCourse;
697
    $sessionId = (int) $sessionId;
698
    $exerciseId = (int) $exerciseId;
699
    $fromExercise = (int) $fromExercise;
700
701
    if ($sessionId < 0) {
702
        $sessionId = 0;
703
    }
704
705
    // If "Select" (-1/0/null) is used, default to the current course to avoid empty results.
706
    if ($selectedCourse <= 0) {
707
        $selectedCourse = (int) api_get_course_int_id();
708
    }
709
710
    $qb = $entityManager->createQueryBuilder();
711
    $qb->from(CQuizQuestion::class, 'qq');
712
713
    // Parameters used by main query and all subqueries
714
    $qb->setParameter('courseId', $selectedCourse);
715
    if ($sessionId > 0) {
716
        $qb->setParameter('sessionId', $sessionId);
717
    }
718
719
    // ---------------------------------------------------------------------
720
    // COURSE SCOPE (C2):
721
    // - Primary: question's own ResourceNode -> ResourceLink -> course
722
    // - Secondary: question used in a quiz belonging to the course
723
    // ---------------------------------------------------------------------
724
    $questionMeta = $entityManager->getClassMetadata(CQuizQuestion::class);
725
726
    $existsViaQuiz = $entityManager->createQueryBuilder();
727
    $existsViaQuiz->select('1')
728
        ->from(CQuizRelQuestion::class, 'rq')
729
        ->innerJoin('rq.quiz', 'q')
730
        ->innerJoin('q.resourceNode', 'qRN')
731
        ->innerJoin('qRN.resourceLinks', 'qRL')
732
        ->where('rq.question = qq')
733
        ->andWhere('IDENTITY(qRL.course) = :courseId');
734
    applyActiveResourceLinkConstraints($existsViaQuiz, 'qRL', $sessionId, true);
735
736
    if ($questionMeta->hasAssociation('resourceNode')) {
737
        $qb->leftJoin('qq.resourceNode', 'rn');
738
739
        $existsQuestionLink = $entityManager->createQueryBuilder();
740
        $existsQuestionLink->select('1')
741
            ->from(\Chamilo\CoreBundle\Entity\ResourceLink::class, 'rl')
742
            ->where('rl.resourceNode = rn')
743
            ->andWhere('IDENTITY(rl.course) = :courseId');
744
        applyActiveResourceLinkConstraints($existsQuestionLink, 'rl', $sessionId, true);
745
746
        $qb->andWhere(
747
            $qb->expr()->orX(
748
                $qb->expr()->exists($existsQuestionLink->getDQL()),
749
                $qb->expr()->exists($existsViaQuiz->getDQL())
750
            )
751
        );
752
    } else {
753
        // Fallback: only include questions used in quizzes belonging to the course.
754
        // Orphan questions without quiz links cannot be discovered in this scenario.
755
        $qb->andWhere($qb->expr()->exists($existsViaQuiz->getDQL()));
756
    }
757
758
    // ---------------------------------------------------------------------
759
    // FILTERS
760
    // ---------------------------------------------------------------------
761
    if ($courseCategoryId > 0) {
762
        $qb->join('qq.categories', 'qc')
763
            ->andWhere('qc.id = :categoryId')
764
            ->setParameter('categoryId', (int) $courseCategoryId);
765
    }
766
767
    if ($exerciseLevel !== null && (int) $exerciseLevel !== -1) {
768
        $qb->andWhere('qq.level = :level')
769
            ->setParameter('level', (int) $exerciseLevel);
770
    }
771
772
    if ($answerType !== null && (int) $answerType > 0) {
773
        $qb->andWhere('qq.type = :type')
774
            ->setParameter('type', (int) $answerType);
775
    }
776
777
    if (!empty($questionId)) {
778
        $qb->andWhere('qq.iid = :questionId')
779
            ->setParameter('questionId', (int) $questionId);
780
    }
781
782
    if (!empty($description)) {
783
        $qb->andWhere('qq.description LIKE :description')
784
            ->setParameter('description', '%'.$description.'%');
785
    }
786
787
    // If a specific quiz is selected, keep only questions in that quiz,
788
    // but only if the quiz is still active in this context (not soft-deleted link).
789
    if ($exerciseId > 0) {
790
        $inQuiz = $entityManager->createQueryBuilder();
791
        $inQuiz->select('1')
792
            ->from(CQuizRelQuestion::class, 'rqq')
793
            ->innerJoin('rqq.quiz', 'qSel')
794
            ->innerJoin('qSel.resourceNode', 'qSelRN')
795
            ->innerJoin('qSelRN.resourceLinks', 'qSelRL')
796
            ->where('IDENTITY(rqq.quiz) = :exerciseId')
797
            ->andWhere('rqq.question = qq')
798
            ->andWhere('IDENTITY(qSelRL.course) = :courseId');
799
        applyActiveResourceLinkConstraints($inQuiz, 'qSelRL', $sessionId, true);
800
801
        $qb->andWhere($qb->expr()->exists($inQuiz->getDQL()))
802
            ->setParameter('exerciseId', $exerciseId);
803
    } elseif ($exerciseId === -1) {
804
        // Orphan: not linked to any ACTIVE quiz in this context.
805
        // A quiz with a soft-deleted link must NOT prevent a question from being orphan.
806
        $hasAnyActive = $entityManager->createQueryBuilder();
807
        $hasAnyActive->select('1')
808
            ->from(CQuizRelQuestion::class, 'rqq2')
809
            ->innerJoin('rqq2.quiz', 'q2')
810
            ->innerJoin('q2.resourceNode', 'q2rn')
811
            ->innerJoin('q2rn.resourceLinks', 'q2rl')
812
            ->where('rqq2.question = qq')
813
            ->andWhere('IDENTITY(q2rl.course) = :courseId');
814
        applyActiveResourceLinkConstraints($hasAnyActive, 'q2rl', $sessionId, true);
815
816
        $qb->andWhere($qb->expr()->not($qb->expr()->exists($hasAnyActive->getDQL())));
817
    }
818
819
    // ---------------------------------------------------------------------
820
    // EXECUTE
821
    // ---------------------------------------------------------------------
822
    if ($getCount) {
823
        $qb->select('COUNT(DISTINCT qq.iid)');
824
825
        try {
826
            return (int) $qb->getQuery()->getSingleScalarResult();
827
        } catch (NoResultException $e) {
828
            return 0;
829
        } catch (\Throwable $e) {
830
            return 0;
831
        }
832
    }
833
834
    $qb->select('qq.iid as id', 'qq.question', 'qq.type', 'qq.level')
835
        ->setFirstResult((int) $start)
836
        ->setMaxResults((int) $length);
837
838
    try {
839
        $results = $qb->getQuery()->getArrayResult();
840
    } catch (\Throwable $e) {
841
        return [];
842
    }
843
844
    $questions = [];
845
    foreach ($results as $result) {
846
        $questions[] = [
847
            'iid' => $result['id'],
848
            'question' => $result['question'],
849
            'type' => $result['type'],
850
            'level' => $result['level'],
851
            // Keep expected shape used later
852
            'exerciseId' => $exerciseId > 0 ? $exerciseId : 0,
853
        ];
854
    }
855
856
    return $questions;
857
}
858
859
$formValues = $form->validate() ? $form->exportValues() : [];
860
861
$nbrQuestions = getQuestions(
862
    true,
863
    null,
864
    null,
865
    $exerciseId,
866
    $courseCategoryId,
867
    $selected_course,
868
    $session_id,
869
    $exerciseLevel,
870
    $answerType,
871
    $questionId,
872
    $description,
873
    $fromExercise,
874
    $formValues
875
);
876
877
$length = (int) api_get_setting('exercise.question_pagination_length');
878
if (empty($length)) {
879
    $length = 20;
880
}
881
$page = (int) $page;
882
$start = ($page - 1) * $length;
883
884
$mainQuestionList = getQuestions(
885
    false,
886
    $start,
887
    $length,
888
    $exerciseId,
889
    $courseCategoryId,
890
    $selected_course,
891
    $session_id,
892
    $exerciseLevel,
893
    $answerType,
894
    $questionId,
895
    $description,
896
    $fromExercise,
897
    $formValues
898
);
899
900
$paginator = new Paginator(Container::$container->get('event_dispatcher'));
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

900
$paginator = new Paginator(Container::$container->/** @scrutinizer ignore-call */ get('event_dispatcher'));

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...
901
$pagination = $paginator->paginate($mainQuestionList, $page, $length);
902
903
$pagination->setTotalItemCount($nbrQuestions);
904
$pagination->setItemNumberPerPage($length);
905
$pagination->setCurrentPageNumber($page);
906
$pagination->renderer = function ($data) use ($url) {
907
    $render = '<nav aria-label="Page navigation" class="question-pool-pagination-nav">';
908
    $render .= '<ul class="pagination">';
909
910
    $link = function ($page, $text, $label, $isActive = false) use ($url) {
911
        $activeClass = $isActive ? ' active' : '';
912
        return '<li class="page-item'.$activeClass.'"><a class="page-link" href="'.$url.'&page='.$page.'" aria-label="'.$label.'">'.$text.'</a></li>';
913
    };
914
915
    if ($data['current'] > 1) {
916
        $render .= $link(1, '&laquo;&laquo;', 'First');
917
        $prevPage = $data['current'] - 1;
918
        $render .= $link($prevPage, '&laquo;', 'Previous');
919
    }
920
921
    $startPage = max(1, $data['current'] - 2);
922
    $endPage = min($data['pageCount'], $data['current'] + 2);
923
    for ($i = $startPage; $i <= $endPage; $i++) {
924
        $render .= $link($i, $i, 'Page '.$i, $data['current'] == $i);
925
    }
926
927
    if ($data['current'] < $data['pageCount']) {
928
        $nextPage = $data['current'] + 1;
929
        $render .= $link($nextPage, '&raquo;', 'Next');
930
        $render .= $link($data['pageCount'], '&raquo;&raquo;', 'Last');
931
    }
932
933
    $render .= '</ul></nav>';
934
935
    return $render;
936
};
937
938
// build the line of the array to display questions
939
/*
940
+--------------------------------------------+--------------------------------------------+
941
|   NOT IN A TEST                            |         IN A TEST                          |
942
+----------------------+---------------------+---------------------+----------------------+
943
|IN THE COURSE (*)  "x | NOT IN THE COURSE o | IN THE COURSE    +  | NOT IN THE COURSE  o |
944
+----------------------+---------------------+---------------------+----------------------+
945
|Edit the question     | Do nothing          | Add question to test|Clone question in test|
946
|Delete the question   |                     |                     |                      |
947
|(true delete)         |                     |                     |                      |
948
+----------------------+---------------------+---------------------+----------------------+
949
(*) this is the only way to delete or modify orphan questions
950
*/
951
952
if ($fromExercise <= 0) {
953
    // OUTSIDE a test → show edit/delete column
954
    $actionLabel = get_lang('Actions');
955
    $actionIcon1 = 'edit';
956
    $actionIcon2 = 'delete';
957
    $questionTagA = 1;
958
} else {
959
    // INSIDE a test → show reuse options
960
    $actionLabel = get_lang('Re-use a copy inside the current test');
961
    $actionIcon1 = 'clone';
962
    $actionIcon2 = 'add';
963
    $questionTagA = 0;
964
965
    if ($selected_course == api_get_course_int_id()) {
966
        $actionLabel = get_lang('Re-use in current test');
967
        $actionIcon1 = 'add';
968
        $actionIcon2 = '';
969
        $questionTagA = 1;
970
    }
971
}
972
973
$data = [];
974
if (is_array($mainQuestionList)) {
975
    foreach ($mainQuestionList as $question) {
976
        $questionId = $question['iid'];
977
        $row = [];
978
979
        // This function checks if the question can be read
980
        $question_type = get_question_type_for_question($selected_course, $questionId);
981
        if (empty($question_type)) {
982
            // Keep legacy behavior: skip rows that cannot build the type icon safely.
983
            continue;
984
        }
985
986
        $sessionId = $question['session_id'] ?? null;
987
988
        if ($fromExercise > 0 && !$objExercise->hasQuestion($question['iid'])) {
989
            $row[] = Display::input(
990
                'checkbox',
991
                'questions[]',
992
                $questionId,
993
                ['class' => 'question_checkbox']
994
            );
995
        } else {
996
            $row[] = '';
997
        }
998
999
        $row[] = getLinkForQuestion(
1000
            $questionTagA,
1001
            $fromExercise,
1002
            $questionId,
1003
            $question['type'],
1004
            $question['question'],
1005
            $sessionId,
1006
            $question['exerciseId']
1007
        );
1008
1009
        $row[] = $question_type;
1010
        $row[] = TestCategory::getCategoryNameForQuestion($questionId, $selected_course);
1011
        $row[] = $question['level'];
1012
1013
        $row[] =
1014
            get_action_icon_for_question(
1015
                $actionIcon1,
1016
                $fromExercise,
1017
                $questionId,
1018
                $question['type'],
1019
                $question['question'],
1020
                $selected_course,
1021
                $courseCategoryId,
1022
                $exerciseLevel,
1023
                $answerType,
1024
                $session_id,
1025
                $question['exerciseId'],
1026
                $objExercise
1027
            ).'&nbsp;'.
1028
            get_action_icon_for_question(
1029
                $actionIcon2,
1030
                $fromExercise,
1031
                $questionId,
1032
                $question['type'],
1033
                $question['question'],
1034
                $selected_course,
1035
                $courseCategoryId,
1036
                $exerciseLevel,
1037
                $answerType,
1038
                $session_id,
1039
                $question['exerciseId'],
1040
                $objExercise
1041
            );
1042
1043
        $data[] = $row;
1044
    }
1045
}
1046
1047
$headers = [
1048
    '',
1049
    get_lang('Question'),
1050
    get_lang('Type'),
1051
    get_lang('Questions category'),
1052
    get_lang('Difficulty'),
1053
    $actionLabel,
1054
];
1055
1056
Display::display_header($nameTools, 'Exercise');
1057
1058
$actions = '';
1059
if (isset($fromExercise) && $fromExercise > 0) {
1060
    $actions .= '<a href="admin.php?'.api_get_cidreq().'&exerciseId='.$fromExercise.'">'.
1061
        Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Go back to the questions list')).'</a>';
1062
} else {
1063
    $actions .= '<a href="exercise.php?'.api_get_cidreq().'">'.
1064
        Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, sprintf(get_lang('Back to %s'), get_lang('Test list'))).'</a>';
1065
    $actions .= '<a href="question_create.php?'.api_get_cidreq().'">'.
1066
        Display::getMdiIcon(ActionIcon::ADD, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('New question')).'</a>';
1067
}
1068
echo Display::toolbarAction('toolbar', [$actions]);
1069
1070
if ('' != $displayMessage) {
1071
    echo Display::return_message($displayMessage, 'confirm');
1072
}
1073
1074
echo $form->display();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $form->display() targeting FormValidator::display() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1075
1076
echo '<script>$(function () {
1077
        '.$jsForExtraFields['jquery_ready_content'].'
1078
    })</script>';
1079
?>
1080
    <div class="clear"></div>
1081
<?php
1082
1083
echo '<div class="text-center">';
1084
echo $pagination;
1085
echo '</div>';
1086
1087
$tableId = 'question_pool_id';
1088
echo '<form id="'.$tableId.'" method="get" action="'.$url.'">';
1089
echo '<input type="hidden" name="fromExercise" value="'.$fromExercise.'">';
1090
echo '<input type="hidden" name="cidReq" value="'.$_course['real_id'].'">';
1091
echo '<input type="hidden" name="cid" value="'.api_get_course_int_id().'">';
1092
echo '<input type="hidden" name="sid" value="'.api_get_session_id().'">';
1093
echo '<input type="hidden" name="selected_course" value="'.$selected_course.'">';
1094
echo '<input type="hidden" name="course_id" value="'.$selected_course.'">';
1095
echo '<input type="hidden" name="action">';
1096
1097
$table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table'], false);
1098
$row = 0;
1099
$column = 0;
1100
$widths = ['10px', '250px', '50px', '200px', '50px', '100px'];
1101
foreach ($headers as $header) {
1102
    $table->setHeaderContents($row, $column, $header);
1103
    $width = array_key_exists($column, $widths) ? $widths[$column] : 'auto';
1104
    $table->setCellAttributes($row, $column, ['style' => "width:$width;"]);
1105
    $column++;
1106
}
1107
1108
$alignments = ['center', 'left', 'center', 'left', 'center', 'center'];
1109
$row = 1;
1110
foreach ($data as $rowData) {
1111
    $column = 0;
1112
    foreach ($rowData as $value) {
1113
        $table->setCellContents($row, $column, $value);
1114
        if (array_key_exists($column, $alignments)) {
1115
            $alignment = $alignments[$column];
1116
            $table->setCellAttributes(
1117
                $row,
1118
                $column,
1119
                ['style' => "text-align:{$alignment};"]
1120
            );
1121
        }
1122
1123
        $column++;
1124
    }
1125
    $row++;
1126
}
1127
$table->display();
1128
echo '</form>';
1129
1130
// --- Bulk actions toolbar (only when we are inside a test) ------------------
1131
$html = '<div class="btn-toolbar question-pool-table-actions">';
1132
$html .= '<div class="btn-group">';
1133
$html .= '<a
1134
        class="btn btn--plain"
1135
        href="?'.$url.'selectall=1"
1136
        onclick="javascript: setCheckbox(true, \''.$tableId.'\'); return false;">
1137
        '.get_lang('Select all').'</a>';
1138
$html .= '<a
1139
            class="btn btn--plain"
1140
            href="?'.$url.'"
1141
            onclick="javascript: setCheckbox(false, \''.$tableId.'\'); return false;">
1142
            '.get_lang('Unselect all').'</a> ';
1143
$html .= '</div>';
1144
1145
if ($fromExercise > 0) {
1146
    $html .= '<div class="btn-group">
1147
                <button class="btn btn--plain action-button">'.get_lang('Actions').'</button>
1148
                <ul class="dropdown-menu" id="action-dropdown" style="display: none;">';
1149
1150
    $actions = ['clone' => get_lang('Re-use a copy inside the current test')];
1151
    if ($selected_course == api_get_course_int_id()) {
1152
        $actions = ['reuse' => get_lang('Re-use in current test')];
1153
    }
1154
1155
    foreach ($actions as $action => $label) {
1156
        $html .= '<li>
1157
                <a
1158
                    data-action ="'.$action.'"
1159
                    href="#"
1160
                    onclick="javascript:action_click(this, \''.$tableId.'\');">'.
1161
            $label.'
1162
                    </a>
1163
                  </li>';
1164
    }
1165
    $html .= '</ul>';
1166
    $html .= '</div>'; //btn-group
1167
}
1168
1169
$html .= '</div>'; //toolbar
1170
1171
echo $html;
1172
1173
Display::display_footer();
1174
1175
/**
1176
 * Put the menu entry for level and type to default "Choice"
1177
 * It is useful if you change the exercise, you need to reset the other menus.
1178
 *
1179
 * @author hubert.borderiou 13-10-2011
1180
 */
1181
function reset_menu_lvl_type()
1182
{
1183
    global $exerciseLevel, $answerType;
1184
1185
    $exerciseLevel = -1;
1186
1187
    if (!isset($_REQUEST['answerType'])) {
1188
        $answerType = -1;
1189
    }
1190
}
1191
1192
/**
1193
 * Put the menu entry for exercise and level and type to default "Choice"
1194
 * It is useful if you change the course, you need to reset the other menus.
1195
 *
1196
 * @author hubert.borderiou 13-10-2011
1197
 */
1198
function reset_menu_exo_lvl_type()
1199
{
1200
    global $exerciseId, $courseCategoryId;
1201
    reset_menu_lvl_type();
1202
    $exerciseId = 0;
1203
    $courseCategoryId = 0;
1204
}
1205
1206
/**
1207
 * return the <a> link to admin question, if needed.
1208
 *
1209
 * @param int    $in_addA
1210
 * @param int    $fromExercise
1211
 * @param int    $questionId
1212
 * @param int    $questionType
1213
 * @param string $questionName
1214
 * @param int    $sessionId
1215
 * @param int    $exerciseId
1216
 *
1217
 * @return string
1218
 *
1219
 * @author hubert.borderiou
1220
 */
1221
function getLinkForQuestion(
1222
    $in_addA,
1223
    $fromExercise,
1224
    $questionId,
1225
    $questionType,
1226
    $questionName,
1227
    $sessionId,
1228
    $exerciseId
1229
) {
1230
    $result = $questionName;
1231
    if ($in_addA) {
1232
        $sessionIcon = '';
1233
        if (!empty($sessionId) && -1 != $sessionId) {
1234
            $sessionIcon = ' '.Display::getMdiIcon(ObjectIcon::STAR, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Session'));
1235
        }
1236
        $exerciseId = (int) $exerciseId;
1237
        $questionId = (int) $questionId;
1238
        $questionType = (int) $questionType;
1239
        $fromExercise = (int) $fromExercise;
1240
1241
        $result = Display::url(
1242
            $questionName.$sessionIcon,
1243
            'admin.php?'.api_get_cidreq().
1244
            "&exerciseId=$exerciseId&editQuestion=$questionId&type=$questionType&fromExercise=$fromExercise"
1245
        );
1246
    }
1247
1248
    return $result;
1249
}
1250
1251
/**
1252
 * Return the <a> html code for delete, add, clone, edit a question.
1253
 */
1254
function get_action_icon_for_question(
1255
    $in_action,
1256
    $from_exercise,
1257
    $in_questionid,
1258
    $in_questiontype,
1259
    $in_questionname,
1260
    $in_selected_course,
1261
    $in_courseCategoryId,
1262
    $in_exerciseLevel,
1263
    $in_answerType,
1264
    $in_session_id,
1265
    $in_exercise_id,
1266
    Exercise $myObjEx
1267
) {
1268
    $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
1269
    $getParams = "&selected_course=$in_selected_course&courseCategoryId=$in_courseCategoryId&exerciseId=$in_exercise_id&exerciseLevel=$in_exerciseLevel&answerType=$in_answerType&session_id=$in_session_id";
1270
    $res = '';
1271
1272
    switch ($in_action) {
1273
        case 'delete':
1274
            if ($limitTeacherAccess && !api_is_platform_admin()) {
1275
                break;
1276
            }
1277
1278
            if (isQuestionInActiveQuiz($in_questionid)) {
1279
                $res = Display::getMdiIcon(
1280
                    ActionIcon::DELETE,
1281
                    'ch-tool-icon-disabled',
1282
                    null,
1283
                    ICON_SIZE_SMALL,
1284
                    get_lang('This question is used in another exercises. If you continue its edition, the changes will affect all exercises that contain this question.')
1285
                );
1286
            } else {
1287
                $res = "<a href='".api_get_self()."?".
1288
                    api_get_cidreq().$getParams."&delete=$in_questionid' onclick='return confirm_your_choice()'>";
1289
                $res .= Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete'));
1290
                $res .= "</a>";
1291
            }
1292
1293
            break;
1294
1295
        case 'edit':
1296
            if (isQuestionInActiveQuiz($in_questionid)) {
1297
                $res = Display::getMdiIcon(
1298
                    ActionIcon::EDIT,
1299
                    'ch-tool-icon-disabled',
1300
                    null,
1301
                    ICON_SIZE_SMALL,
1302
                    get_lang('This question belongs to a test. Edit it from inside the test or filter Orphan questions.')
1303
                );
1304
                break;
1305
            }
1306
1307
            $res = getLinkForQuestion(
1308
                1,
1309
                $from_exercise,
1310
                $in_questionid,
1311
                $in_questiontype,
1312
                Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')),
1313
                $in_session_id,
1314
                $in_exercise_id
1315
            );
1316
            break;
1317
1318
        case 'add':
1319
            $res = '-';
1320
            if ($from_exercise > 0 && !$myObjEx->hasQuestion($in_questionid)) {
1321
                $res = "<a href='".api_get_self().'?'.
1322
                    api_get_cidreq().$getParams."&recup=$in_questionid&fromExercise=$from_exercise'>";
1323
                $res .= Display::getMdiIcon(ActionIcon::VIEW_DETAILS, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Use this question in the test as a link (not a copy)'));
1324
                $res .= '</a>';
1325
            }
1326
            break;
1327
1328
        case 'clone':
1329
            $url = api_get_self().'?'.api_get_cidreq().$getParams.
1330
                "&question_copy=$in_questionid&course_id=$in_selected_course&fromExercise=$from_exercise";
1331
            $res = Display::url(
1332
                Display::getMdiIcon(ActionIcon::COPY_CONTENT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Re-use a copy inside the current test')),
1333
                $url
1334
            );
1335
            break;
1336
1337
        default:
1338
            // When no action is expected, return empty string to keep layout clean.
1339
            $res = '';
1340
            break;
1341
    }
1342
1343
    return $res;
1344
}
1345
1346
/**
1347
 * Checks whether a question is used by any ACTIVE quiz in the current context.
1348
 *
1349
 * Soft-delete aware:
1350
 * - A quiz that only exists through a soft-deleted ResourceLink must NOT block question deletion.
1351
 */
1352
function isQuestionInActiveQuiz($questionId)
1353
{
1354
    global $selected_course, $session_id;
1355
1356
    $questionId = (int) $questionId;
1357
    if (empty($questionId)) {
1358
        return false;
1359
    }
1360
1361
    $courseId = (int) $selected_course;
1362
    if ($courseId <= 0) {
1363
        $courseId = (int) api_get_course_int_id();
1364
    }
1365
1366
    $sessionId = (int) $session_id;
1367
    if ($sessionId < 0) {
1368
        $sessionId = 0;
1369
    }
1370
1371
    try {
1372
        $entityManager = Database::getManager();
1373
1374
        $qb = $entityManager->createQueryBuilder();
1375
        $qb->select('COUNT(DISTINCT q.iid)')
1376
            ->from(CQuizRelQuestion::class, 'rqq')
1377
            ->innerJoin('rqq.quiz', 'q')
1378
            ->innerJoin('q.resourceNode', 'rn')
1379
            ->innerJoin('rn.resourceLinks', 'rl')
1380
            ->where('rqq.question = :questionId')
1381
            ->andWhere('IDENTITY(rl.course) = :courseId')
1382
            ->setParameter('questionId', $questionId)
1383
            ->setParameter('courseId', $courseId);
1384
1385
        // Soft-delete aware "active link" rules.
1386
        // NOTE: If you want session-only scope, change last param to false.
1387
        applyActiveResourceLinkConstraints($qb, 'rl', $sessionId, true);
1388
1389
        if ($sessionId > 0) {
1390
            $qb->setParameter('sessionId', $sessionId);
1391
        }
1392
1393
        $count = (int) $qb->getQuery()->getSingleScalarResult();
1394
1395
        return $count > 0;
1396
    } catch (\Throwable $e) {
1397
        error_log('[question_pool] isQuestionInActiveQuiz failed: '.$e->getMessage());
1398
        // Fail-safe: keep legacy safe behavior (treat as used).
1399
        return true;
1400
    }
1401
}
1402
1403
/**
1404
 * Return the icon for the question type.
1405
 *
1406
 * @author hubert.borderiou 13-10-2011
1407
 */
1408
function get_question_type_for_question($in_selectedcourse, $in_questionid)
1409
{
1410
    $courseInfo = api_get_course_info_by_id($in_selectedcourse);
1411
    $question = Question::read($in_questionid, $courseInfo);
1412
    $questionType = null;
1413
    if (!empty($question)) {
1414
        $typeImg = $question->getTypePicture();
1415
        $typeExpl = $question->getExplanation();
1416
1417
        $questionType = Display::tag('div', Display::return_icon($typeImg, $typeExpl, [], 32), []);
1418
    }
1419
1420
    return $questionType;
1421
}
1422