Passed
Push — 1.11.x ( b616c4...c8e71a )
by Julito
14:46
created

ScoreDisplay::displayDefault()   C

Complexity

Conditions 14
Paths 15

Size

Total Lines 56
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 40
nc 15
nop 4
dl 0
loc 56
rs 6.2666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

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
        $value = $value['my_display_coloring'];
35
36
        // Setting coloring.
37
        $this->coloring_enabled = $value === 'true' ? true : false;
38
39
        if ($this->coloring_enabled) {
40
            $value = api_get_setting('gradebook_score_display_colorsplit');
41
            if (isset($value)) {
42
                $this->color_split_value = $value;
43
            }
44
        }
45
46
        // Setting custom enabled
47
        $value = api_get_setting('gradebook_score_display_custom');
48
        $value = $value['my_display_custom'];
49
        $this->custom_enabled = $value === 'true' ? true : false;
50
51
        if ($this->custom_enabled) {
52
            $params = ['category = ?' => ['Gradebook']];
53
            $displays = api_get_settings_params($params);
54
            $portal_displays = [];
55
            if (!empty($displays)) {
56
                foreach ($displays as $display) {
57
                    $data = explode('::', $display['selected_value']);
58
                    if (empty($data[1])) {
59
                        $data[1] = '';
60
                    }
61
                    $portal_displays[$data[0]] = [
62
                        'score' => $data[0],
63
                        'display' => $data[1],
64
                    ];
65
                }
66
                sort($portal_displays);
67
            }
68
            $this->custom_display = $portal_displays;
69
            if (count($this->custom_display) > 0) {
70
                $value = api_get_setting('gradebook_score_display_upperlimit');
71
                $value = $value['my_display_upperlimit'];
72
                $this->upperlimit_included = $value === 'true' ? true : false;
73
                $this->custom_display_conv = $this->convert_displays($this->custom_display);
74
            }
75
        }
76
77
        // If teachers can override the portal parameters
78
        if (api_get_setting('teachers_can_change_score_settings') === 'true') {
79
            //Load course settings
80
            if ($this->custom_enabled) {
81
                $this->custom_display = $this->get_custom_displays();
82
                if (count($this->custom_display) > 0) {
83
                    $this->custom_display_conv = $this->convert_displays($this->custom_display);
84
                }
85
            }
86
87
            if ($this->coloring_enabled) {
88
                $this->color_split_value = $this->get_score_color_percent();
89
            }
90
        }
91
    }
92
93
    /**
94
     * Get the instance of this class.
95
     *
96
     * @param int $categoryId
97
     *
98
     * @return ScoreDisplay
99
     */
100
    public static function instance($categoryId = 0)
101
    {
102
        static $instance;
103
        if (!isset($instance)) {
104
            $instance = new ScoreDisplay($categoryId);
105
        }
106
107
        return $instance;
108
    }
109
110
    /**
111
     * Compare the custom display of 2 scores, can be useful in sorting.
112
     */
113
    public static function compare_scores_by_custom_display($score1, $score2)
114
    {
115
        if (!isset($score1)) {
116
            return isset($score2) ? 1 : 0;
117
        }
118
119
        if (!isset($score2)) {
120
            return -1;
121
        }
122
123
        $scoreDisplay = self::instance();
124
        $custom1 = $scoreDisplay->display_custom($score1);
125
        $custom2 = $scoreDisplay->display_custom($score2);
126
        if ($custom1 == $custom2) {
127
            return 0;
128
        }
129
130
        return ($score1[0] / $score1[1]) < ($score2[0] / $score2[1]) ? -1 : 1;
131
    }
132
133
    /**
134
     * Is coloring enabled ?
135
     */
136
    public function is_coloring_enabled()
137
    {
138
        return $this->coloring_enabled;
139
    }
140
141
    /**
142
     * Is custom score display enabled ?
143
     */
144
    public function is_custom()
145
    {
146
        return $this->custom_enabled;
147
    }
148
149
    /**
150
     * Is upperlimit included ?
151
     */
152
    public function is_upperlimit_included()
153
    {
154
        return $this->upperlimit_included;
155
    }
156
157
    /**
158
     * If custom score display is enabled, this will return the current settings.
159
     * See also updateCustomScoreDisplaySettings.
160
     *
161
     * @return array current settings (or null if feature not enabled)
162
     */
163
    public function get_custom_score_display_settings()
164
    {
165
        return $this->custom_display;
166
    }
167
168
    /**
169
     * If coloring is enabled, scores below this value will be displayed in red.
170
     *
171
     * @return int color split value, in percent (or null if feature not enabled)
172
     */
173
    public function get_color_split_value()
174
    {
175
        return $this->color_split_value;
176
    }
177
178
    /**
179
     * Update custom score display settings.
180
     *
181
     * @param array $displays 2-dimensional array - every sub array must have keys (score, display)
182
     * @param int   score color percent (optional)
183
     * @param int   gradebook category id (optional)
184
     */
185
    public function updateCustomScoreDisplaySettings(
186
        $displays,
187
        $scorecolpercent = 0,
188
        $category_id = null
189
    ) {
190
        $this->custom_display = $displays;
191
        $this->custom_display_conv = $this->convert_displays($this->custom_display);
192
        if (isset($category_id)) {
193
            $category_id = (int) $category_id;
194
        } else {
195
            $category_id = $this->get_current_gradebook_category_id();
196
        }
197
198
        // remove previous settings
199
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY);
200
        $sql = 'DELETE FROM '.$table.' WHERE category_id = '.$category_id;
201
        Database::query($sql);
202
203
        // add new settings
204
        foreach ($displays as $display) {
205
            $params = [
206
                'score' => $display['score'],
207
                'display' => $display['display'],
208
                'category_id' => $category_id,
209
                'score_color_percent' => $scorecolpercent,
210
            ];
211
            Database::insert($table, $params);
212
        }
213
    }
214
215
    /**
216
     * @param int $category_id
217
     *
218
     * @return false|null
219
     */
220
    public function insert_defaults($category_id)
221
    {
222
        if (empty($category_id)) {
223
            return false;
224
        }
225
226
        //Get this from DB settings
227
        $display = [
228
            50 => get_lang('GradebookFailed'),
229
            60 => get_lang('GradebookPoor'),
230
            70 => get_lang('GradebookFair'),
231
            80 => get_lang('GradebookGood'),
232
            90 => get_lang('GradebookOutstanding'),
233
            100 => get_lang('GradebookExcellent'),
234
        ];
235
236
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY);
237
        foreach ($display as $value => $text) {
238
            $params = [
239
                'score' => $value,
240
                'display' => $text,
241
                'category_id' => $category_id,
242
                'score_color_percent' => 0,
243
            ];
244
            Database::insert($table, $params);
245
        }
246
    }
247
248
    /**
249
     * @return int
250
     */
251
    public function get_number_decimals()
252
    {
253
        $number_decimals = api_get_setting('gradebook_number_decimals');
254
        if (!isset($number_decimals)) {
255
            $number_decimals = 0;
256
        }
257
258
        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...
259
    }
260
261
    /**
262
     * Formats a number depending of the number of decimals.
263
     *
264
     * @param float  $score
265
     * @param bool   $ignoreDecimals
266
     * @param string $decimalSeparator
267
     * @param string $thousandSeparator
268
     * @param bool $removeEmptyDecimals Converts 100.00 to 100, 53.00 to 53
269
     *
270
     * @return float the score formatted
271
     */
272
    public function format_score(
273
        $score,
274
        $ignoreDecimals = false,
275
        $decimalSeparator = '.',
276
        $thousandSeparator = ',',
277
        $removeEmptyDecimals = false
278
    ) {
279
        $decimals = $this->get_number_decimals();
280
        if ($ignoreDecimals) {
281
            $decimals = 0;
282
        }
283
284
        if ($removeEmptyDecimals) {
285
            if ($score && self::hasEmptyDecimals($score)) {
286
                $score = round($score);
287
                $decimals = 0;
288
            }
289
        }
290
291
        return api_number_format($score, $decimals, $decimalSeparator, $thousandSeparator);
292
    }
293
294
    public static function hasEmptyDecimals($score)
295
    {
296
        $hasEmptyDecimals = false;
297
        if (is_float($score)) {
298
            $check = fmod($score, 1);
299
            if (0 === bccomp(0, $check)) {
300
                $score = round($score);
301
                $hasEmptyDecimals = true;
302
            }
303
        }
304
        if (is_int($score) || is_string($score)) {
305
            $score = (float) $score;
306
            $check = fmod($score, 1);
307
            if (0 === bccomp(0, $check)) {
308
                $hasEmptyDecimals = true;
309
            }
310
        }
311
312
        return $hasEmptyDecimals;
313
    }
314
315
    /**
316
     * Display a score according to the current settings.
317
     *
318
     * @param array $score          data structure, as returned by the calc_score functions
319
     * @param int   $type           one of the following constants:
320
     *                              SCORE_DIV, SCORE_PERCENT, SCORE_DIV_PERCENT, SCORE_AVERAGE
321
     *                              (ignored for student's view if custom score display is enabled)
322
     * @param int   $what           one of the following constants:
323
     *                              SCORE_BOTH, SCORE_ONLY_DEFAULT, SCORE_ONLY_CUSTOM (default: SCORE_BOTH)
324
     *                              (only taken into account if custom score display is enabled and for course/platform admin)
325
     * @param bool  $disableColor
326
     * @param bool  $ignoreDecimals
327
     * @param bool  $removeEmptyDecimals Replaces 100.00 to 100
328
     *
329
     * @return string
330
     */
331
    public function display_score(
332
        $score,
333
        $type = SCORE_DIV_PERCENT,
334
        $what = SCORE_BOTH,
335
        $disableColor = false,
336
        $ignoreDecimals = false,
337
        $removeEmptyDecimals = false
338
    ) {
339
        $my_score = $score == 0 ? [] : $score;
340
        switch ($type) {
341
            case SCORE_BAR:
342
                $percentage = $my_score[0] / $my_score[1] * 100;
343
344
                return Display::bar_progress($percentage);
345
                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...
346
            case SCORE_NUMERIC:
347
                $percentage = $my_score[0] / $my_score[1] * 100;
348
349
                return round($percentage);
350
                break;
351
            case SCORE_SIMPLE:
352
                if (!isset($my_score[0])) {
353
                    $my_score[0] = 0;
354
                }
355
                return $this->format_score($my_score[0], $ignoreDecimals);
356
                break;
357
        }
358
359
        if ($this->custom_enabled && isset($this->custom_display_conv)) {
360
            $display = $this->displayDefault($my_score, $type, $ignoreDecimals, $removeEmptyDecimals);
361
        } else {
362
            // if no custom display set, use default display
363
            $display = $this->displayDefault($my_score, $type, $ignoreDecimals, $removeEmptyDecimals);
364
        }
365
        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...
366
            $denom = isset($score[1]) && !empty($score[1]) && $score[1] > 0 ? $score[1] : 1;
367
            $scoreCleaned = isset($score[0]) ? $score[0] : 0;
368
            if (($scoreCleaned / $denom) < ($this->color_split_value / 100)) {
369
                $display = Display::tag(
370
                    'font',
371
                    $display,
372
                    ['color' => 'red']
373
                );
374
            }
375
        }
376
377
        return $display;
378
    }
379
380
    /**
381
     * Depends on the teacher's configuration of thresholds. i.e. [0 50] "Bad", [50:100] "Good".
382
     *
383
     * @param array $score
384
     *
385
     * @return string
386
     */
387
    public function display_custom($score)
388
    {
389
        if (empty($score)) {
390
            return null;
391
        }
392
393
        $denom = $score[1] == 0 ? 1 : $score[1];
394
        $scaledscore = $score[0] / $denom;
395
396
        if ($this->upperlimit_included) {
397
            foreach ($this->custom_display_conv as $displayitem) {
398
                if ($scaledscore <= $displayitem['score']) {
399
                    return $displayitem['display'];
400
                }
401
            }
402
        } else {
403
            if (!empty($this->custom_display_conv)) {
404
                foreach ($this->custom_display_conv as $displayitem) {
405
                    if ($scaledscore < $displayitem['score'] || $displayitem['score'] == 1) {
406
                        return $displayitem['display'];
407
                    }
408
                }
409
            }
410
        }
411
    }
412
413
    /**
414
     * Get current gradebook category id.
415
     *
416
     * @return int Category id
417
     */
418
    private function get_current_gradebook_category_id()
419
    {
420
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
421
        $courseCode = api_get_course_id();
422
        $sessionId = api_get_session_id();
423
        $sessionCondition = api_get_session_condition($sessionId, true);
424
425
        $sql = "SELECT id FROM $table
426
                WHERE course_code = '$courseCode'  $sessionCondition";
427
        $rs = Database::query($sql);
428
        $categoryId = 0;
429
        if (Database::num_rows($rs) > 0) {
430
            $row = Database::fetch_row($rs);
431
            $categoryId = $row[0];
432
        }
433
434
        return $categoryId;
435
    }
436
437
    /**
438
     * @param array $score
439
     * @param int   $type
440
     * @param bool  $ignoreDecimals
441
     * @param bool  $removeEmptyDecimals
442
     *
443
     * @return string
444
     */
445
    private function displayDefault($score, $type, $ignoreDecimals = false, $removeEmptyDecimals = false)
446
    {
447
        switch ($type) {
448
            case SCORE_DIV:                            // X / Y
449
                return $this->display_as_div($score, $ignoreDecimals, $removeEmptyDecimals);
450
            case SCORE_PERCENT:                        // XX %
451
                return $this->display_as_percent($score);
452
            case SCORE_DIV_PERCENT:                    // X / Y (XX %)
453
                //return $this->display_as_div($score).' ('.$this->display_as_percent($score).')';
454
                // 2020-10 Changed to XX % (X / Y)
455
                return $this->display_as_percent($score).' ('.$this->display_as_div($score).')';
456
            case SCORE_AVERAGE:                        // XX %
457
                return $this->display_as_percent($score);
458
            case SCORE_DECIMAL:                        // 0.50  (X/Y)
459
                return $this->display_as_decimal($score);
460
            case SCORE_DIV_PERCENT_WITH_CUSTOM:        // X / Y (XX %) - Good!
461
                $custom = $this->display_custom($score);
462
                if (!empty($custom)) {
463
                    $custom = ' - '.$custom;
464
                }
465
                $div = $this->display_as_div($score, false, $removeEmptyDecimals);
466
                /*return
467
                    $div.
468
                    ' ('.$this->display_as_percent($score).')'.$custom;*/
469
                return
470
                    $this->display_as_percent($score).
471
                    ' ('.$div.')'.$custom;
472
            case SCORE_DIV_SIMPLE_WITH_CUSTOM:         // X - Good!
473
                $custom = $this->display_custom($score);
474
475
                if (!empty($custom)) {
476
                    $custom = ' - '.$custom;
477
                }
478
479
                return $this->display_simple_score($score).$custom;
480
                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...
481
            case SCORE_DIV_SIMPLE_WITH_CUSTOM_LETTERS:
482
                $custom = $this->display_custom($score);
483
                if (!empty($custom)) {
484
                    $custom = ' - '.$custom;
485
                }
486
                $score = $this->display_simple_score($score);
487
488
                //needs sudo apt-get install php5-intl
489
                if (class_exists('NumberFormatter')) {
490
                    $iso = api_get_language_isocode();
491
                    $f = new NumberFormatter($iso, NumberFormatter::SPELLOUT);
492
                    $letters = $f->format($score);
493
                    $letters = api_strtoupper($letters);
494
                    $letters = " ($letters) ";
495
                }
496
497
                return $score.$letters.$custom;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $letters does not seem to be defined for all execution paths leading up to this point.
Loading history...
498
                break;
499
            case SCORE_CUSTOM:                          // Good!
500
                return $this->display_custom($score);
501
        }
502
    }
503
504
    /**
505
     * @param array $score
506
     *
507
     * @return float|string
508
     */
509
    private function display_simple_score($score)
510
    {
511
        if (isset($score[0])) {
512
            return $this->format_score($score[0]);
513
        }
514
515
        return '';
516
    }
517
518
    /**
519
     * Returns "1" for array("100", "100").
520
     *
521
     * @param array $score
522
     *
523
     * @return float
524
     */
525
    private function display_as_decimal($score)
526
    {
527
        $score_denom = $score[1] == 0 ? 1 : $score[1];
528
529
        return $this->format_score($score[0] / $score_denom);
530
    }
531
532
    /**
533
     * Returns "100 %" for array("100", "100").
534
     */
535
    private function display_as_percent($score)
536
    {
537
        if (empty($score)) {
538
            return null;
539
        }
540
        $scoreDenom = $score[1] == 0 ? 1 : $score[1];
541
542
        return $this->format_score($score[0] / $scoreDenom * 100).' %';
543
    }
544
545
    /**
546
     * Returns 10.00 / 10.00 for array("100", "100").
547
     *
548
     * @param array $score
549
     * @param bool  $ignoreDecimals
550
     * @param bool  $removeEmptyDecimals
551
     *
552
     * @return string
553
     */
554
    private function display_as_div($score, $ignoreDecimals = false, $removeEmptyDecimals = false)
555
    {
556
        if ($score == 1) {
557
            return '0 / 0';
558
        }
559
560
        if (empty($score)) {
561
            return '0 / 0';
562
        }
563
564
        $score[0] = isset($score[0]) ? $this->format_score($score[0], $ignoreDecimals) : 0;
565
        $score[1] = isset($score[1]) ? $this->format_score(
566
            $score[1],
567
            $ignoreDecimals,
568
            '.',
569
            ',',
570
            $removeEmptyDecimals
571
        ) : 0;
572
573
        return $score[0].' / '.$score[1];
574
    }
575
576
    /**
577
     * Get score color percent by category.
578
     *
579
     * @param   int Gradebook category id
580
     *
581
     * @return int Score
582
     */
583
    private function get_score_color_percent($category_id = null)
584
    {
585
        $tbl_display = Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY);
586
        if (isset($category_id)) {
587
            $category_id = (int) $category_id;
588
        } else {
589
            $category_id = $this->get_current_gradebook_category_id();
590
        }
591
592
        $sql = 'SELECT score_color_percent FROM '.$tbl_display.'
593
                WHERE category_id = '.$category_id.'
594
                LIMIT 1';
595
        $result = Database::query($sql);
596
        $score = 0;
597
        if (Database::num_rows($result) > 0) {
598
            $row = Database::fetch_row($result);
599
            $score = $row[0];
600
        }
601
602
        return $score;
603
    }
604
605
    /**
606
     * Get current custom score display settings.
607
     *
608
     * @param   int     Gradebook category id
609
     *
610
     * @return array 2-dimensional array every element contains 3 subelements (id, score, display)
611
     */
612
    private function get_custom_displays($category_id = null)
613
    {
614
        $tbl_display = Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY);
615
        if (isset($category_id)) {
616
            $category_id = (int) $category_id;
617
        } else {
618
            $category_id = $this->get_current_gradebook_category_id();
619
        }
620
        $sql = 'SELECT * FROM '.$tbl_display.'
621
                WHERE category_id = '.$category_id.'
622
                ORDER BY score';
623
        $result = Database::query($sql);
624
625
        return Database::store_result($result, 'ASSOC');
626
    }
627
628
    /**
629
     * Convert display settings to internally used values.
630
     */
631
    private function convert_displays($custom_display)
632
    {
633
        if (isset($custom_display)) {
634
            // get highest score entry, and copy each element to a new array
635
            $converted = [];
636
            $highest = 0;
637
            foreach ($custom_display as $element) {
638
                if ($element['score'] > $highest) {
639
                    $highest = $element['score'];
640
                }
641
                $converted[] = $element;
642
            }
643
            // sort the new array (ascending)
644
            usort($converted, ['ScoreDisplay', 'sort_display']);
645
646
            // adjust each score in such a way that
647
            // each score is scaled between 0 and 1
648
            // the highest score in this array will be equal to 1
649
            $converted2 = [];
650
            foreach ($converted as $element) {
651
                $newelement = [];
652
                if (isset($highest) && !empty($highest) && $highest > 0) {
653
                    $newelement['score'] = $element['score'] / $highest;
654
                } else {
655
                    $newelement['score'] = 0;
656
                }
657
                $newelement['display'] = $element['display'];
658
                $converted2[] = $newelement;
659
            }
660
661
            return $converted2;
662
        }
663
664
        return null;
665
    }
666
667
    /**
668
     * @param array $item1
669
     * @param array $item2
670
     *
671
     * @return int
672
     */
673
    private function sort_display($item1, $item2)
674
    {
675
        if ($item1['score'] === $item2['score']) {
676
            return 0;
677
        } else {
678
            return $item1['score'] < $item2['score'] ? -1 : 1;
679
        }
680
    }
681
}
682