Passed
Pull Request — master (#7026)
by
unknown
09:30
created

_compute_course_limit_state_by_code()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
/**
6
 *	This script allows platform admins to add users to courses.
7
 *	It displays a list of users and a list of courses;
8
 *	you can select multiple users and courses and then click on
9
 *	'Add to this(these) course(s)'.
10
 *
11
 * 	@todo use formvalidator for the form
12
 */
13
14
use Chamilo\CoreBundle\Enums\ObjectIcon;
15
16
$cidReset = true;
17
require_once __DIR__.'/../inc/global.inc.php';
18
$this_section = SECTION_PLATFORM_ADMIN;
19
20
api_protect_admin_script();
21
22
$form_sent = 0;
23
$first_letter_user = '';
24
$first_letter_course = '';
25
$courses = [];
26
$users = [];
27
28
$tbl_course = Database::get_main_table(TABLE_MAIN_COURSE);
29
$tbl_user = Database::get_main_table(TABLE_MAIN_USER);
30
31
/* Header */
32
$tool_name = get_lang('Add users to course');
33
$interbreadcrumb[] = ['url' => 'index.php', 'name' => get_lang('Administration')];
34
35
$htmlHeadXtra[] = '<script>
36
function validate_filter() {
37
  document.formulaire.form_sent.value=0;
38
  document.formulaire.submit();
39
}
40
</script>';
41
42
// displaying the header
43
Display::display_header($tool_name);
44
45
$link_add_group = '<a href="usergroups.php">'.
46
    Display::getMdiIcon(ObjectIcon::MULTI_ELEMENT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Enrolment by classes')).get_lang('Enrolment by classes').'</a>';
47
echo Display::toolbarAction('subscribe', [$link_add_group]);
48
49
/**
50
 * We show this once at the top so admins are aware before selecting anything.
51
 */
52
$__globalLimit = (int) api_get_setting('platform.hosting_limit_users_per_course'); // 0 => disabled
53
if ($__globalLimit > 0) {
54
    echo Display::return_message(
55
        sprintf('A global limit of %d users applies to every course (teachers included).', $__globalLimit),
56
        'warning'
57
    );
58
}
59
60
$form = new FormValidator('subscribe_user2course');
61
$form->addElement('header', '', $tool_name);
62
$form->display();
63
64
//checking for extra field with filter on
65
$extra_field_list = UserManager::get_extra_fields();
66
67
$new_field_list = [];
68
if (is_array($extra_field_list)) {
69
    foreach ($extra_field_list as $extra_field) {
70
        // if is enabled to filter and is a "<select>" or "tag" type
71
        if (1 == $extra_field[8] && ExtraField::FIELD_TYPE_SELECT == $extra_field[2]) {
72
            $new_field_list[] = [
73
                'name' => $extra_field[3],
74
                'type' => $extra_field[2],
75
                'variable' => $extra_field[1],
76
                'data' => $extra_field[9],
77
            ];
78
        }
79
        if (1 == $extra_field[8] && ExtraField::FIELD_TYPE_TAG == $extra_field[2]) {
80
            $options = UserManager::get_extra_user_data_for_tags($extra_field[1]);
81
82
            $new_field_list[] = [
83
                'name' => $extra_field[3],
84
                'type' => $extra_field[2],
85
                'variable' => $extra_field[1],
86
                'data' => $options['options'],
87
            ];
88
        }
89
    }
90
}
91
92
/* React on POSTed request */
93
if (isset($_POST['form_sent']) && $_POST['form_sent']) {
94
    $form_sent = (int) $_POST['form_sent'];
95
    $users = isset($_POST['UserList']) && is_array($_POST['UserList']) ? $_POST['UserList'] : [];
96
    $courses = isset($_POST['CourseList']) && is_array($_POST['CourseList']) ? $_POST['CourseList'] : [];
97
    $first_letter_user = Database::escape_string($_POST['firstLetterUser']);
98
    $first_letter_course = Database::escape_string($_POST['firstLetterCourse']);
99
100
    foreach ($users as $key => $value) {
101
        $users[$key] = (int) $value;
102
    }
103
104
    if (1 === $form_sent) {
105
        if (0 === count($users) || 0 === count($courses)) {
106
            echo Display::return_message(get_lang('You must select at least one user and one course'), 'error');
107
        } else {
108
            $errorDrh = 0;
109
            $successCount = 0;
110
            $skippedFull = 0;
111
112
            foreach ($courses as $course_code) {
113
                $courseInfo = api_get_course_info($course_code);
114
                if (empty($courseInfo)) {
115
                    // Defensive log
116
                    Display::addFlash(Display::return_message('Course not found: '.$course_code, 'warning'));
117
                    continue;
118
                }
119
120
                // Enforce global limit here as well, to avoid needless subscribe calls
121
                if ($__globalLimit > 0) {
122
                    $limitState = _compute_course_limit_state_by_real_id($courseInfo['real_id'], $__globalLimit);
123
                    if ($limitState['full']) {
124
                        // Avoid looping users for a known-full course, provide a single message and skip
125
                        Display::addFlash(Display::return_message(
126
                            sprintf('Course "%s" is full (%d/%d). Skipping subscriptions for this course.',
127
                                $courseInfo['title'], $limitState['current'], $limitState['limit']
128
                            ),
129
                            'warning'
130
                        ));
131
                        $skippedFull++;
132
                        continue;
133
                    }
134
                }
135
136
                foreach ($users as $user_id) {
137
                    $user = api_get_user_info($user_id);
138
                    if (DRH != $user['status']) {
139
                        $result = CourseManager::subscribeUser($user_id, $courseInfo['real_id']);
140
141
                        if (is_array($result)) {
142
                            // Expected keys: ok(bool), message(string)
143
                            if (isset($result['message'])) {
144
                                Display::addFlash(
145
                                    Display::return_message($result['message'], !empty($result['ok']) ? 'normal' : 'warning')
146
                                );
147
                            } else {
148
                                // assume ok by presence of array
149
                                $successCount++;
150
                            }
151
                        } else {
152
                            if ($result === true) {
153
                                $successCount++;
154
                            }
155
                        }
156
157
                    } else {
158
                        $errorDrh = 1;
159
                    }
160
                }
161
            }
162
163
            // Summaries
164
            if ($successCount > 0) {
165
                echo Display::return_message(
166
                    sprintf(get_lang('The selected users are subscribed to the selected course').' (%d %s)', $successCount, get_lang('operations')),
167
                    'confirm'
168
                );
169
            }
170
171
            if ($skippedFull > 0) {
172
                echo Display::return_message(
173
                    sprintf('%d course(s) skipped because they are full.', $skippedFull),
174
                    'warning'
175
                );
176
            }
177
178
            if (1 === $errorDrh) {
179
                echo Display::return_message(
180
                    get_lang(
181
                        'Human resources managers should not be registered to courses. The corresponding users you selected have not been subscribed.'
182
                    ),
183
                    'error'
184
                );
185
            }
186
        }
187
    }
188
}
189
190
/* Display GUI */
191
if (empty($first_letter_user)) {
192
    $sql = "SELECT count(*) as nb_users FROM $tbl_user";
193
    $result = Database::query($sql);
194
    $num_row = Database::fetch_array($result);
195
    if ($num_row['nb_users'] > 1000) {
196
        // If there are too many users, default filter to "A" to keep lists light
197
        $first_letter_user = 'A';
198
    }
199
    unset($result);
200
}
201
202
$where_filter = null;
203
$extra_field_result = [];
204
//Filter by Extra Fields
205
$use_extra_fields = false;
206
if (is_array($extra_field_list)) {
207
    if (is_array($new_field_list) && count($new_field_list) > 0) {
208
        $result_list = [];
209
        foreach ($new_field_list as $new_field) {
210
            $varname = 'field_'.$new_field['variable'];
211
            $fieldtype = $new_field['type'];
212
            if (UserManager::is_extra_field_available($new_field['variable'])) {
213
                if (isset($_POST[$varname]) && '0' != $_POST[$varname]) {
214
                    $use_extra_fields = true;
215
                    if (ExtraField::FIELD_TYPE_TAG == $fieldtype) {
216
                        $extra_field_result[] = UserManager::get_extra_user_data_by_tags(
217
                            (int) $_POST['field_id'],
218
                            $_POST[$varname]
219
                        );
220
                    } else {
221
                        $extra_field_result[] = UserManager::get_extra_user_data_by_value(
222
                            $new_field['variable'],
223
                            $_POST[$varname]
224
                        );
225
                    }
226
                }
227
            }
228
        }
229
    }
230
}
231
232
if ($use_extra_fields) {
233
    $final_result = [];
234
    if (count($extra_field_result) > 1) {
235
        for ($i = 0; $i < count($extra_field_result) - 1; $i++) {
236
            if (is_array($extra_field_result[$i + 1])) {
237
                $final_result = array_intersect($extra_field_result[$i], $extra_field_result[$i + 1]);
238
            }
239
        }
240
    } else {
241
        $final_result = $extra_field_result[0];
242
    }
243
244
    if (api_is_multiple_url_enabled()) {
0 ignored issues
show
Deprecated Code introduced by
The function api_is_multiple_url_enabled() has been deprecated: Use AccessUrlUtil::isMultiple ( Ignorable by Annotation )

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

244
    if (/** @scrutinizer ignore-deprecated */ api_is_multiple_url_enabled()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
245
        if (is_array($final_result) && count($final_result) > 0) {
246
            $where_filter = " AND u.id IN  ('".implode("','", $final_result)."') ";
247
        } else {
248
            $where_filter = " AND u.id  = -1";
249
        }
250
    } else {
251
        if (is_array($final_result) && count($final_result) > 0) {
252
            $where_filter = " AND id IN  ('".implode("','", $final_result)."') ";
253
        } else {
254
            $where_filter = " AND id  = -1";
255
        }
256
    }
257
}
258
259
$target_name = 'lastname';
260
$orderBy = $target_name;
261
$showOfficialCode = false;
262
$orderListByOfficialCode = api_get_setting('display.order_user_list_by_official_code');
263
if ('true' === $orderListByOfficialCode) {
264
    $showOfficialCode = true;
265
    $orderBy = " official_code, lastname, firstname";
266
}
267
268
$sql = "SELECT id as user_id, lastname, firstname, username, official_code
269
        FROM $tbl_user
270
        WHERE id <>2 AND ".$target_name." LIKE '".$first_letter_user."%' $where_filter
271
        ORDER BY ".(count($users) > 0 ? "(id IN(".implode(',', $users).")) DESC," : "")." ".$orderBy;
272
273
if (api_is_multiple_url_enabled()) {
0 ignored issues
show
Deprecated Code introduced by
The function api_is_multiple_url_enabled() has been deprecated: Use AccessUrlUtil::isMultiple ( Ignorable by Annotation )

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

273
if (/** @scrutinizer ignore-deprecated */ api_is_multiple_url_enabled()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
274
    $tbl_user_rel_access_url = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
275
    $access_url_id = api_get_current_access_url_id();
276
    if (-1 != $access_url_id) {
277
        $sql = "SELECT u.id as user_id,lastname,firstname,username, official_code
278
                FROM $tbl_user u
279
                INNER JOIN $tbl_user_rel_access_url user_rel_url
280
                ON (user_rel_url.user_id = u.id)
281
                WHERE
282
                    u.id <> 2 AND
283
                    access_url_id =  $access_url_id AND
284
                    (".$target_name." LIKE '".$first_letter_user."%' )
285
                    $where_filter
286
                ORDER BY ".(count($users) > 0 ? "(u.id IN(".implode(',', $users).")) DESC," : "")." ".$orderBy;
287
    }
288
}
289
290
$result = Database::query($sql);
291
$db_users = Database::store_result($result);
292
unset($result);
293
294
$sql = "SELECT code,visual_code,title
295
        FROM $tbl_course
296
        WHERE visual_code LIKE '".$first_letter_course."%'
297
        ORDER BY ".(count($courses) > 0 ? "(code IN('".implode("','", $courses)."')) DESC," : "")." visual_code";
298
299
if (api_is_multiple_url_enabled()) {
0 ignored issues
show
Deprecated Code introduced by
The function api_is_multiple_url_enabled() has been deprecated: Use AccessUrlUtil::isMultiple ( Ignorable by Annotation )

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

299
if (/** @scrutinizer ignore-deprecated */ api_is_multiple_url_enabled()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
300
    $tbl_course_rel_access_url = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
301
    $access_url_id = api_get_current_access_url_id();
302
    if (-1 != $access_url_id) {
303
        $sql = "SELECT code, visual_code, title
304
                FROM $tbl_course as course
305
                INNER JOIN $tbl_course_rel_access_url course_rel_url
306
                ON (course_rel_url.c_id = course.id)
307
                WHERE
308
                    access_url_id =  $access_url_id  AND
309
                    (visual_code LIKE '".$first_letter_course."%' )
310
                ORDER BY ".(count($courses) > 0 ? "(code IN('".implode("','", $courses)."')) DESC," : "")." visual_code";
311
    }
312
}
313
314
$result = Database::query($sql);
315
$db_courses = Database::store_result($result);
316
unset($result);
317
?>
318
    <form name="formulaire" method="post" action="<?php echo api_get_self(); ?>" class="w-full px-4 sm:px-6 lg:px-8">
319
        <input type="hidden" name="form_sent" value="1"/>
320
321
        <?php
322
        if (is_array($extra_field_list)) {
323
            if (is_array($new_field_list) && count($new_field_list) > 0) {
324
                echo '<div class="mb-6 rounded-2xl border border-gray-25 p-4 bg-white shadow-sm w-full overflow-hidden">';
325
                echo '<h3 class="text-lg font-semibold mb-3">'.get_lang('Filter users').'</h3>';
326
                echo '<div class="flex flex-wrap gap-3 min-w-0">';
327
                foreach ($new_field_list as $new_field) {
328
                    echo '<label class="text-sm font-medium">'.htmlspecialchars($new_field['name']).'</label>';
329
                    $varname = 'field_'.$new_field['variable'];
330
                    $fieldtype = $new_field['type'];
331
332
                    echo '<select name="'.$varname.'" class="form-select rounded-xl border border-gray-25 px-3 py-2 text-sm w-full sm:w-auto max-w-full">';
333
                    echo '<option value="0">--'.get_lang('Select').'--</option>';
334
                    foreach ($new_field['data'] as $option) {
335
                        $checked = '';
336
                        if (ExtraField::FIELD_TYPE_TAG == $fieldtype) {
337
                            if (isset($_POST[$varname]) && $_POST[$varname] == $option['tag']) {
338
                                $checked = 'selected="true"';
339
                            }
340
                            echo '<option value="'.Security::remove_XSS($option['tag']).'" '.$checked.'>'.$option['tag'].'</option>';
341
                        } else {
342
                            if (isset($_POST[$varname]) && $_POST[$varname] == $option[1]) {
343
                                $checked = 'selected="true"';
344
                            }
345
                            echo '<option value="'.Security::remove_XSS($option[1]).'" '.$checked.'>'.$option[2].'</option>';
346
                        }
347
                    }
348
                    echo '</select>';
349
                    $extraHidden = ExtraField::FIELD_TYPE_TAG == $fieldtype ? '<input type="hidden" name="field_id" value="'.(int) $option['field_id'].'" />' : '';
350
                    echo $extraHidden;
351
                }
352
                echo '</div>';
353
                echo '<div class="mt-4">';
354
                echo '<button type="button" onclick="validate_filter()" class="inline-flex items-center rounded-2xl bg-primary px-4 py-2 text-white text-sm font-medium hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary">'.get_lang('Filter').'</button>';
355
                echo '</div>';
356
                echo '</div>';
357
            }
358
        }
359
        ?>
360
361
        <div class="w-full flex flex-col md:flex-row items-stretch gap-6">
362
363
            <!-- Users pane (left) -->
364
            <div class="flex-1 md:basis-5/12 min-w-[420px] rounded-2xl border border-gray-25 p-4 bg-white shadow-sm">
365
                <label class="block text-sm font-medium mb-2"><?php echo get_lang('User list'); ?></label>
366
367
                <div class="flex items-center gap-2 mb-3">
368
                    <span class="text-sm text-gray-90"><?php echo get_lang('First letter (last name)'); ?>:</span>
369
                    <select name="firstLetterUser"
370
                            onchange="document.formulaire.form_sent.value='2'; document.formulaire.submit();"
371
                            aria-label="<?php echo get_lang('First letter (last name)'); ?>"
372
                            class="rounded-xl border border-gray-25 px-3 py-1 text-sm w-auto">
373
                        <option value="">--</option>
374
                        <?php echo Display::get_alphabet_options($first_letter_user); ?>
375
                    </select>
376
                </div>
377
378
                <select name="UserList[]" multiple size="20"
379
                        class="block w-full max-w-none min-w-[400px] h-[28rem] rounded-2xl border border-gray-25 bg-white px-3 py-2 text-sm">
380
                    <?php foreach ($db_users as $user) { ?>
381
                        <option value="<?php echo (int) $user['user_id']; ?>" <?php if (in_array($user['user_id'], $users)) echo 'selected="selected"'; ?>>
382
                            <?php
383
                            $userName = $user['lastname'].' '.$user['firstname'].' ('.$user['username'].')';
384
                            if ($showOfficialCode) {
385
                                $officialCode = !empty($user['official_code']) ? $user['official_code'].' - ' : '? - ';
386
                                $userName = $officialCode.$userName;
387
                            }
388
                            echo Security::remove_XSS($userName);
389
                            ?>
390
                        </option>
391
                    <?php } ?>
392
                </select>
393
            </div>
394
395
            <!-- Center action -->
396
            <div class="md:basis-2/12 flex items-center justify-center">
397
                <button type="submit"
398
                        class="w-full md:w-auto inline-flex items-center justify-center rounded-2xl bg-primary px-6 py-3 text-white font-semibold shadow hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary">
399
                    <svg class="mr-2 h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 5v14m-7-7h14"/></svg>
400
                    <?php echo get_lang('Add to the course(s)'); ?>
401
                </button>
402
            </div>
403
404
            <!-- Courses pane (right) -->
405
            <div class="flex-1 md:basis-5/12 min-w-[420px] rounded-2xl border border-gray-25 p-4 bg-white shadow-sm">
406
                <label class="block text-sm font-medium mb-2"><?php echo get_lang('Course list'); ?></label>
407
408
                <div class="flex items-center gap-2 mb-3">
409
                    <span class="text-sm text-gray-90"><?php echo get_lang('First letter (code)'); ?>:</span>
410
                    <select name="firstLetterCourse"
411
                            onchange="document.formulaire.form_sent.value='2'; document.formulaire.submit();"
412
                            aria-label="<?php echo get_lang('First letter (code)'); ?>"
413
                            class="rounded-xl border border-gray-25 px-3 py-1 text-sm w-auto">
414
                        <option value="">--</option>
415
                        <?php echo Display::get_alphabet_options($first_letter_course); ?>
416
                    </select>
417
                </div>
418
419
                <select name="CourseList[]" multiple size="20"
420
                        class="block w-full max-w-none min-w-[400px] h-[28rem] rounded-2xl border border-gray-25 bg-white px-3 py-2 text-sm">
421
                    <?php foreach ($db_courses as $course) {
422
                        $suffix = '';
423
                        if ($__globalLimit > 0) {
424
                            $state = _compute_course_limit_state_by_code($course['code'], $__globalLimit);
425
                            $suffix = $state['full']
426
                                ? ' — [full '.$state['current'].'/'.$state['limit'].']'
427
                                : ' — [seats left: '.$state['seatsLeft'].'/'.$state['limit'].']';
428
                        } ?>
429
                        <option value="<?php echo Security::remove_XSS($course['code']); ?>" <?php if (in_array($course['code'], $courses)) echo 'selected="selected"'; ?>>
430
                            <?php echo '('.Security::remove_XSS($course['visual_code']).') '.Security::remove_XSS($course['title']).$suffix; ?>
431
                        </option>
432
                    <?php } ?>
433
                </select>
434
            </div>
435
436
        </div>
437
438
439
    </form>
440
<?php
441
442
Display::display_footer();
443
444
/**
445
 * Compute occupancy state by course code (for option labels).
446
 *
447
 * @return array{limit:int,current:int,seatsLeft:int,full:bool}
448
 */
449
function _compute_course_limit_state_by_code(string $courseCode, int $globalLimit): array
450
{
451
    $info = api_get_course_info($courseCode);
452
    if (empty($info) || $globalLimit <= 0) {
453
        return ['limit' => max(0, $globalLimit), 'current' => 0, 'seatsLeft' => $globalLimit, 'full' => false];
454
    }
455
    return _compute_course_limit_state_by_real_id((int) $info['real_id'], $globalLimit);
456
}
457
458
/**
459
 * Compute occupancy state by real course id (for precheck and labels).
460
 * Counts all users (teachers included), excluding RRHH relation type.
461
 */
462
function _compute_course_limit_state_by_real_id(int $courseRealId, int $globalLimit): array
463
{
464
    $current = 0;
465
    if ($globalLimit > 0) {
466
        $sqlCount = "SELECT COUNT(*) AS total
467
                     FROM ".Database::get_main_table(TABLE_MAIN_COURSE_USER)."
468
                     WHERE c_id = $courseRealId
469
                       AND relation_type <> ".COURSE_RELATION_TYPE_RRHH;
470
        $row = Database::fetch_array(Database::query($sqlCount), 'ASSOC');
471
        $current = (int) ($row['total'] ?? 0);
472
    }
473
    $seatsLeft = max(0, $globalLimit - $current);
474
    return [
475
        'limit' => $globalLimit,
476
        'current' => $current,
477
        'seatsLeft' => $seatsLeft,
478
        'full' => $globalLimit > 0 && $current >= $globalLimit,
479
    ];
480
}
481