Passed
Push — master ( 668bf0...77aaf8 )
by Julito
11:36 queued 12s
created

ScoreDisplay::display_as_percent()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
/**
6
 * Class ScoreDisplay
7
 * Display scores according to the settings made by the platform admin.
8
 * This class works as a singleton: call instance() to retrieve an object.
9
 *
10
 * @author Bert Steppé
11
 */
12
class ScoreDisplay
13
{
14
    private $coloring_enabled;
15
    private $color_split_value;
16
    private $custom_enabled;
17
    private $upperlimit_included;
18
    private $custom_display;
19
    private $custom_display_conv;
20
21
    /**
22
     * Protected constructor - call instance() to instantiate.
23
     *
24
     * @param int $category_id
25
     */
26
    public function __construct($category_id = 0)
27
    {
28
        if (!empty($category_id)) {
29
            $this->category_id = $category_id;
30
        }
31
32
        // Loading portal settings + using standard functions.
33
        $value = api_get_setting('gradebook_score_display_coloring');
34
35
        // Setting coloring.
36
        $this->coloring_enabled = 'true' == $value ? true : false;
37
38
        if ($this->coloring_enabled) {
39
            $value = api_get_setting('gradebook_score_display_colorsplit');
40
            if (isset($value)) {
41
                $this->color_split_value = $value;
42
            }
43
        }
44
45
        // Setting custom enabled
46
        $value = api_get_setting('gradebook_score_display_custom');
47
        $this->custom_enabled = 'true' == $value ? true : false;
48
49
        if ($this->custom_enabled) {
50
            $params = ['category = ?' => ['Gradebook']];
51
            $displays = api_get_settings_params($params);
52
            $portal_displays = [];
53
            if (!empty($displays)) {
54
                foreach ($displays as $display) {
55
                    $data = explode('::', $display['selected_value']);
56
                    if (empty($data[1])) {
57
                        $data[1] = '';
58
                    }
59
                    $portal_displays[$data[0]] = [
60
                        'score' => $data[0],
61
                        'display' => $data[1],
62
                    ];
63
                }
64
                sort($portal_displays);
65
            }
66
            $this->custom_display = $portal_displays;
67
            if (count($this->custom_display) > 0) {
68
                $value = api_get_setting('gradebook_score_display_upperlimit');
69
                $value = $value['my_display_upperlimit'];
70
                $this->upperlimit_included = 'true' == $value ? true : false;
71
                $this->custom_display_conv = $this->convert_displays($this->custom_display);
72
            }
73
        }
74
75
        //If teachers can override the portal parameters
76
        if ('true' == api_get_setting('teachers_can_change_score_settings')) {
77
            //Load course settings
78
            if ($this->custom_enabled) {
79
                $this->custom_display = $this->get_custom_displays();
80
                if (count($this->custom_display) > 0) {
81
                    $this->custom_display_conv = $this->convert_displays($this->custom_display);
82
                }
83
            }
84
85
            if ($this->coloring_enabled) {
86
                $this->color_split_value = $this->get_score_color_percent();
87
            }
88
        }
89
    }
90
91
    /**
92
     * Get the instance of this class.
93
     *
94
     * @param int $categoryId
95
     *
96
     * @return ScoreDisplay
97
     */
98
    public static function instance($categoryId = 0)
99
    {
100
        static $instance;
101
        if (!isset($instance)) {
102
            $instance = new ScoreDisplay($categoryId);
103
        }
104
105
        return $instance;
106
    }
107
108
    /**
109
     * Compare the custom display of 2 scores, can be useful in sorting.
110
     */
111
    public static function compare_scores_by_custom_display($score1, $score2)
112
    {
113
        if (!isset($score1)) {
114
            return isset($score2) ? 1 : 0;
115
        }
116
117
        if (!isset($score2)) {
118
            return -1;
119
        }
120
121
        $scoreDisplay = self::instance();
122
        $custom1 = $scoreDisplay->display_custom($score1);
123
        $custom2 = $scoreDisplay->display_custom($score2);
124
        if ($custom1 == $custom2) {
125
            return 0;
126
        }
127
128
        return ($score1[0] / $score1[1]) < ($score2[0] / $score2[1]) ? -1 : 1;
129
    }
130
131
    /**
132
     * Is coloring enabled ?
133
     */
134
    public function is_coloring_enabled()
135
    {
136
        return $this->coloring_enabled;
137
    }
138
139
    /**
140
     * Is custom score display enabled ?
141
     */
142
    public function is_custom()
143
    {
144
        return $this->custom_enabled;
145
    }
146
147
    /**
148
     * Is upperlimit included ?
149
     */
150
    public function is_upperlimit_included()
151
    {
152
        return $this->upperlimit_included;
153
    }
154
155
    /**
156
     * If custom score display is enabled, this will return the current settings.
157
     * See also updateCustomScoreDisplaySettings.
158
     *
159
     * @return array current settings (or null if feature not enabled)
160
     */
161
    public function get_custom_score_display_settings()
162
    {
163
        return $this->custom_display;
164
    }
165
166
    /**
167
     * If coloring is enabled, scores below this value will be displayed in red.
168
     *
169
     * @return int color split value, in percent (or null if feature not enabled)
170
     */
171
    public function get_color_split_value()
172
    {
173
        return $this->color_split_value;
174
    }
175
176
    /**
177
     * Update custom score display settings.
178
     *
179
     * @param array $displays 2-dimensional array - every sub array must have keys (score, display)
180
     * @param int   score color percent (optional)
181
     * @param int   gradebook category id (optional)
182
     */
183
    public function updateCustomScoreDisplaySettings(
184
        $displays,
185
        $scorecolpercent = 0,
186
        $category_id = null
187
    ) {
188
        $this->custom_display = $displays;
189
        $this->custom_display_conv = $this->convert_displays($this->custom_display);
190
        if (isset($category_id)) {
191
            $category_id = (int) $category_id;
192
        } else {
193
            $category_id = $this->get_current_gradebook_category_id();
194
        }
195
196
        // remove previous settings
197
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY);
198
        $sql = 'DELETE FROM '.$table.' WHERE category_id = '.$category_id;
199
        Database::query($sql);
200
201
        // add new settings
202
        foreach ($displays as $display) {
203
            $params = [
204
                'score' => $display['score'],
205
                'display' => $display['display'],
206
                'category_id' => $category_id,
207
                'score_color_percent' => $scorecolpercent,
208
            ];
209
            Database::insert($table, $params);
210
        }
211
    }
212
213
    /**
214
     * @param int $category_id
215
     *
216
     * @return false|null
217
     */
218
    public function insert_defaults($category_id)
219
    {
220
        if (empty($category_id)) {
221
            return false;
222
        }
223
224
        //Get this from DB settings
225
        $display = [
226
            50 => get_lang('Failed'),
227
            60 => get_lang('Poor'),
228
            70 => get_lang('Fair'),
229
            80 => get_lang('Good'),
230
            90 => get_lang('Outstanding'),
231
            100 => get_lang('Excellent'),
232
        ];
233
234
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY);
235
        foreach ($display as $value => $text) {
236
            $params = [
237
                'score' => $value,
238
                'display' => $text,
239
                'category_id' => $category_id,
240
                'score_color_percent' => 0,
241
            ];
242
            Database::insert($table, $params);
243
        }
244
    }
245
246
    /**
247
     * @return int
248
     */
249
    public function get_number_decimals()
250
    {
251
        $number_decimals = api_get_setting('gradebook_number_decimals');
252
        if (!isset($number_decimals)) {
253
            $number_decimals = 0;
254
        }
255
256
        return $number_decimals;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $number_decimals also could return the type string which is incompatible with the documented return type integer.
Loading history...
257
    }
258
259
    /**
260
     * Formats a number depending of the number of decimals.
261
     *
262
     * @param float  $score
263
     * @param bool   $ignoreDecimals
264
     * @param string $decimalSeparator
265
     * @param string $thousandSeparator
266
     * @param bool   $removeEmptyDecimals Converts 100.00 to 100, 53.00 to 53
267
     *
268
     * @return float the score formatted
269
     */
270
    public function format_score(
271
        $score,
272
        $ignoreDecimals = false,
273
        $decimalSeparator = '.',
274
        $thousandSeparator = ',',
275
        $removeEmptyDecimals = false
276
    ) {
277
        $decimals = $this->get_number_decimals();
278
        if ($ignoreDecimals) {
279
            $decimals = 0;
280
        }
281
282
        if ($removeEmptyDecimals) {
283
            if ($score && self::hasEmptyDecimals($score)) {
284
                $score = round($score);
285
                $decimals = 0;
286
            }
287
        }
288
289
        return api_number_format($score, $decimals, $decimalSeparator, $thousandSeparator);
290
    }
291
292
    public static function hasEmptyDecimals($score)
293
    {
294
        $hasEmptyDecimals = false;
295
        if (is_float($score)) {
296
            $check = fmod($score, 1);
297
            if (0 === bccomp(0, $check)) {
298
                $score = round($score);
299
                $hasEmptyDecimals = true;
300
            }
301
        }
302
        if (is_int($score) || is_string($score)) {
303
            $score = (float) $score;
304
            $check = fmod($score, 1);
305
            if (0 === bccomp(0, $check)) {
306
                $hasEmptyDecimals = true;
307
            }
308
        }
309
310
        return $hasEmptyDecimals;
311
    }
312
    /**
313
     * Display a score according to the current settings.
314
     *
315
     * @param array $score          data structure, as returned by the calc_score functions
316
     * @param int   $type           one of the following constants:
317
     *                              SCORE_DIV, SCORE_PERCENT, SCORE_DIV_PERCENT, SCORE_AVERAGE
318
     *                              (ignored for student's view if custom score display is enabled)
319
     * @param int   $what           one of the following constants:
320
     *                              SCORE_BOTH, SCORE_ONLY_DEFAULT, SCORE_ONLY_CUSTOM (default: SCORE_BOTH)
321
     *                              (only taken into account if custom score display is enabled and for course/platform admin)
322
     * @param bool  $disableColor
323
     * @param bool  $ignoreDecimals
324
     * @param bool  $removeEmptyDecimals Replaces 100.00 to 100
325
     *
326
     * @return string
327
     */
328
    public function display_score(
329
        $score,
330
        $type = SCORE_DIV_PERCENT,
331
        $what = SCORE_BOTH,
332
        $disableColor = false,
333
        $ignoreDecimals = false,
334
        $removeEmptyDecimals = false
335
    ) {
336
        $my_score = $score == 0 ? [] : $score;
337
338
        switch ($type) {
339
            case SCORE_BAR:
340
                $percentage = $my_score[0] / $my_score[1] * 100;
341
342
                return Display::bar_progress($percentage);
343
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
344
            case SCORE_NUMERIC:
345
346
                $percentage = $my_score[0] / $my_score[1] * 100;
347
348
                return round($percentage);
349
                break;
350
            case SCORE_SIMPLE:
351
                if (!isset($my_score[0])) {
352
                    $my_score[0] = 0;
353
                }
354
                return $this->format_score($my_score[0], $ignoreDecimals);
355
                break;
356
        }
357
358
        if ($this->custom_enabled && isset($this->custom_display_conv)) {
359
            $display = $this->displayDefault($my_score, $type, $ignoreDecimals, $removeEmptyDecimals);
360
        } else {
361
            // if no custom display set, use default display
362
            $display = $this->displayDefault($my_score, $type, $ignoreDecimals, $removeEmptyDecimals);
363
        }
364
        if ($this->coloring_enabled && $disableColor == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
365
            $denom = isset($score[1]) && !empty($score[1]) && $score[1] > 0 ? $score[1] : 1;
366
            $scoreCleaned = isset($score[0]) ? $score[0] : 0;
367
            if (($scoreCleaned / $denom) < ($this->color_split_value / 100)) {
368
                $display = Display::tag(
369
                    'font',
370
                    $display,
371
                    ['color' => 'red']
372
                );
373
            }
374
        }
375
376
        return $display;
377
    }
378
379
    /**
380
     * Depends on the teacher's configuration of thresholds. i.e. [0 50] "Bad", [50:100] "Good".
381
     *
382
     * @param array $score
383
     *
384
     * @return string
385
     */
386
    public function display_custom($score)
387
    {
388
        if (empty($score)) {
389
            return null;
390
        }
391
392
        $denom = $score[1] == 0 ? 1 : $score[1];
393
        $scaledscore = $score[0] / $denom;
394
395
        if ($this->upperlimit_included) {
396
            foreach ($this->custom_display_conv as $displayitem) {
397
                if ($scaledscore <= $displayitem['score']) {
398
                    return $displayitem['display'];
399
                }
400
            }
401
        } else {
402
            if (!empty($this->custom_display_conv)) {
403
                foreach ($this->custom_display_conv as $displayitem) {
404
                    if ($scaledscore < $displayitem['score'] || $displayitem['score'] == 1) {
405
                        return $displayitem['display'];
406
                    }
407
                }
408
            }
409
        }
410
    }
411
412
    /**
413
     * Get current gradebook category id.
414
     *
415
     * @return int Category id
416
     */
417
    private function get_current_gradebook_category_id()
418
    {
419
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
420
        $courseId = api_get_course_int_id();
421
        $sessionId = api_get_session_id();
422
        $sessionCondition = api_get_session_condition($sessionId, true);
423
424
        $sql = "SELECT id FROM $table
425
                WHERE c_id = '$courseId'  $sessionCondition";
426
        $rs = Database::query($sql);
427
        $categoryId = 0;
428
        if (Database::num_rows($rs) > 0) {
429
            $row = Database::fetch_row($rs);
430
            $categoryId = $row[0];
431
        }
432
433
        return $categoryId;
434
    }
435
436
    /**
437
     * @param array $score
438
     * @param int  $type
439
     * @param bool $ignoreDecimals
440
     * @param bool  $removeEmptyDecimals
441
     *
442
     * @return string
443
     */
444
    private function displayDefault($score, $type, $ignoreDecimals = false, $removeEmptyDecimals = false)
445
    {
446
        switch ($type) {
447
            case SCORE_DIV:                            // X / Y
448
                return $this->display_as_div($score, $ignoreDecimals, $removeEmptyDecimals);
449
            case SCORE_PERCENT:                        // XX %
450
                return $this->display_as_percent($score);
451
            case SCORE_DIV_PERCENT:                    // X / Y (XX %)
452
                return $this->display_as_percent($score).' ('.$this->display_as_div($score).')';
453
            case SCORE_AVERAGE:                        // XX %
454
                return $this->display_as_percent($score);
455
            case SCORE_DECIMAL:                        // 0.50  (X/Y)
456
                return $this->display_as_decimal($score);
457
            case SCORE_DIV_PERCENT_WITH_CUSTOM:        // X / Y (XX %) - Good!
458
                $custom = $this->display_custom($score);
459
                if (!empty($custom)) {
460
                    $custom = ' - '.$custom;
461
                }
462
463
                $div = $this->display_as_div($score, false, $removeEmptyDecimals);
464
                /*return
465
                    $div.
466
                    ' ('.$this->display_as_percent($score).')'.$custom;*/
467
                return
468
                    $this->display_as_percent($score).
469
                    ' ('.$div.')'.$custom;
470
            case SCORE_DIV_SIMPLE_WITH_CUSTOM:         // X - Good!
471
                $custom = $this->display_custom($score);
472
473
                if (!empty($custom)) {
474
                    $custom = ' - '.$custom;
475
                }
476
477
                return $this->display_simple_score($score).$custom;
478
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
479
            case SCORE_DIV_SIMPLE_WITH_CUSTOM_LETTERS:
480
                $custom = $this->display_custom($score);
481
                if (!empty($custom)) {
482
                    $custom = ' - '.$custom;
483
                }
484
                $score = $this->display_simple_score($score);
485
486
                $iso = api_get_language_isocode();
487
                $f = new NumberFormatter($iso, NumberFormatter::SPELLOUT);
488
                $letters = $f->format($score);
489
                $letters = api_strtoupper($letters);
490
                $letters = " ($letters) ";
491
492
                return $score.$letters.$custom;
493
                break;
494
            case SCORE_CUSTOM:                          // Good!
495
                return $this->display_custom($score);
496
        }
497
    }
498
499
    /**
500
     * @param array $score
501
     *
502
     * @return float|string
503
     */
504
    private function display_simple_score($score)
505
    {
506
        if (isset($score[0])) {
507
            return $this->format_score($score[0]);
508
        }
509
510
        return '';
511
    }
512
513
    /**
514
     * Returns "1" for array("100", "100").
515
     *
516
     * @param array $score
517
     *
518
     * @return float
519
     */
520
    private function display_as_decimal($score)
521
    {
522
        $score_denom = (0 == $score[1]) ? 1 : $score[1];
523
524
        return $this->format_score($score[0] / $score_denom);
525
    }
526
527
    /**
528
     * Returns "100 %" for array("100", "100").
529
     */
530
    private function display_as_percent($score)
531
    {
532
        if (empty($score)) {
533
            return null;
534
        }
535
        $scoreDenom = $score[1] == 0 ? 1 : $score[1];
536
537
        return $this->format_score($score[0] / $scoreDenom * 100).' %';
538
    }
539
540
    /**
541
     * Returns 10.00 / 10.00 for array("100", "100").
542
     *
543
     * @param array $score
544
     * @param bool  $ignoreDecimals
545
     * @param bool  $removeEmptyDecimals
546
     *
547
     * @return string
548
     */
549
    private function display_as_div($score, $ignoreDecimals = false, $removeEmptyDecimals = false)
550
    {
551
        if ($score == 1) {
552
            return '0 / 0';
553
        }
554
555
        if (empty($score)) {
556
            return '0 / 0';
557
        }
558
559
        $score[0] = isset($score[0]) ? $this->format_score($score[0], $ignoreDecimals) : 0;
560
        $score[1] = isset($score[1]) ? $this->format_score(
561
            $score[1],
562
            $ignoreDecimals,
563
            '.',
564
            ',',
565
            $removeEmptyDecimals
566
        ) : 0;
567
568
        return  $score[0].' / '.$score[1];
569
    }
570
571
    /**
572
     * Get score color percent by category.
573
     *
574
     * @param   int Gradebook category id
575
     *
576
     * @return int Score
577
     */
578
    private function get_score_color_percent($category_id = null)
579
    {
580
        $tbl_display = Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY);
581
        if (isset($category_id)) {
582
            $category_id = (int) $category_id;
583
        } else {
584
            $category_id = $this->get_current_gradebook_category_id();
585
        }
586
587
        $sql = 'SELECT score_color_percent FROM '.$tbl_display.'
588
                WHERE category_id = '.$category_id.'
589
                LIMIT 1';
590
        $result = Database::query($sql);
591
        $score = 0;
592
        if (Database::num_rows($result) > 0) {
593
            $row = Database::fetch_row($result);
594
            $score = $row[0];
595
        }
596
597
        return $score;
598
    }
599
600
    /**
601
     * Get current custom score display settings.
602
     *
603
     * @param   int     Gradebook category id
604
     *
605
     * @return array 2-dimensional array every element contains 3 subelements (id, score, display)
606
     */
607
    private function get_custom_displays($category_id = null)
608
    {
609
        $tbl_display = Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY);
610
        if (isset($category_id)) {
611
            $category_id = (int) $category_id;
612
        } else {
613
            $category_id = $this->get_current_gradebook_category_id();
614
        }
615
        $sql = 'SELECT * FROM '.$tbl_display.'
616
                WHERE category_id = '.$category_id.'
617
                ORDER BY score';
618
        $result = Database::query($sql);
619
620
        return Database::store_result($result, 'ASSOC');
621
    }
622
623
    /**
624
     * Convert display settings to internally used values.
625
     */
626
    private function convert_displays($custom_display)
627
    {
628
        if (isset($custom_display)) {
629
            // get highest score entry, and copy each element to a new array
630
            $converted = [];
631
            $highest = 0;
632
            foreach ($custom_display as $element) {
633
                if ($element['score'] > $highest) {
634
                    $highest = $element['score'];
635
                }
636
                $converted[] = $element;
637
            }
638
            // sort the new array (ascending)
639
            usort($converted, ['ScoreDisplay', 'sort_display']);
640
641
            // adjust each score in such a way that
642
            // each score is scaled between 0 and 1
643
            // the highest score in this array will be equal to 1
644
            $converted2 = [];
645
            foreach ($converted as $element) {
646
                $newelement = [];
647
                if (isset($highest) && !empty($highest) && $highest > 0) {
648
                    $newelement['score'] = $element['score'] / $highest;
649
                } else {
650
                    $newelement['score'] = 0;
651
                }
652
                $newelement['display'] = $element['display'];
653
                $converted2[] = $newelement;
654
            }
655
656
            return $converted2;
657
        }
658
            return null;
659
    }
660
661
    /**
662
     * @param array $item1
663
     * @param array $item2
664
     *
665
     * @return int
666
     */
667
    private function sort_display($item1, $item2)
668
    {
669
        if ($item1['score'] === $item2['score']) {
670
            return 0;
671
        } else {
672
            return $item1['score'] < $item2['score'] ? -1 : 1;
673
        }
674
    }
675
}
676