Passed
Push — master ( fe335e...c46b0e )
by
unknown
20:15 queued 10:02
created

ResultTable::build_edit_column()   B

Complexity

Conditions 8
Paths 24

Size

Total Lines 47
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 33
c 0
b 0
f 0
nc 24
nop 1
dl 0
loc 47
rs 8.1475
1
<?php
2
/* For licensing terms, see /license.txt */
3
4
use Chamilo\CoreBundle\Enums\ActionIcon;
5
use Chamilo\CoreBundle\Enums\ToolIcon;
6
7
/**
8
 * Class ResultTable
9
 * Table to display results for an evaluation.
10
 *
11
 * @author Stijn Konings
12
 * @author Bert Steppé
13
 */
14
class ResultTable extends SortableTable
15
{
16
    private $datagen;
17
    private $evaluation;
18
    private $allresults;
0 ignored issues
show
introduced by
The private property $allresults is not used, and could be removed.
Loading history...
19
    private $iscourse;
20
21
    /**
22
     * ResultTable constructor.
23
     *
24
     * @param Evaluation   $evaluation
25
     * @param ?array       $results
26
     * @param ?string      $iscourse
27
     * @param ?array       $addparams
28
     * @param ?bool        $forprint
29
     */
30
    public function __construct(
31
        Evaluation $evaluation,
32
        ?array $results = [],
33
        ?string $iscourse = '0',
34
        ?array $addparams = [],
35
        ?bool $forprint = false
36
    ) {
37
        parent:: __construct(
38
            'resultlist',
39
            null,
40
            null,
41
            api_is_western_name_order() ? 1 : 2
42
        );
43
44
        $this->datagen = new ResultsDataGenerator($evaluation, $results, true);
45
46
        $this->evaluation = $evaluation;
47
        $this->iscourse = $iscourse;
48
        $this->forprint = $forprint;
49
50
        if (isset($addparams)) {
51
            $this->set_additional_parameters($addparams);
52
        }
53
        $scoredisplay = ScoreDisplay::instance();
54
        $column = 0;
55
        if ('1' == $this->iscourse) {
56
            $this->set_header($column++, '', false);
57
            $this->set_form_actions([
58
                    'delete' => get_lang('Delete'),
59
            ]);
60
        }
61
        if (api_is_western_name_order()) {
62
            $this->set_header($column++, get_lang('First name'));
63
            $this->set_header($column++, get_lang('Last name'));
64
        } else {
65
            $this->set_header($column++, get_lang('Last name'));
66
            $this->set_header($column++, get_lang('First name'));
67
        }
68
69
        $model = ExerciseLib::getCourseScoreModel();
70
        if (empty($model)) {
71
            $this->set_header($column++, get_lang('Score'));
72
        }
73
74
        if ($scoredisplay->is_custom()) {
75
            $this->set_header($column++, get_lang('Ranking'));
76
        }
77
        if (!$this->forprint) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->forprint of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
78
            $this->set_header($column++, get_lang('Edit'), false);
79
        }
80
    }
81
82
    /**
83
     * Function used by SortableTable to get total number of items in the table.
84
     */
85
    public function get_total_number_of_items()
86
    {
87
        return $this->datagen->get_total_results_count();
88
    }
89
90
    /**
91
     * Function used by SortableTable to generate the data to display.
92
     */
93
    public function get_table_data(
94
        $from = 1,
95
        $perPage = null,
96
        $column = null,
97
        $direction = null,
98
        $sort = null
99
    ) {
100
        $isWesternNameOrder = api_is_western_name_order();
101
        $scoredisplay = ScoreDisplay::instance();
102
103
        // determine sorting type
104
        $col_adjust = '1' == $this->iscourse ? 1 : 0;
105
106
        switch ($this->column) {
107
            // first name or last name
108
            case 0 + $col_adjust:
109
                if ($isWesternNameOrder) {
110
                    $sorting = ResultsDataGenerator::RDG_SORT_FIRSTNAME;
111
                } else {
112
                    $sorting = ResultsDataGenerator::RDG_SORT_LASTNAME;
113
                }
114
                break;
115
                // first name or last name
116
            case 1 + $col_adjust:
117
                if ($isWesternNameOrder) {
118
                    $sorting = ResultsDataGenerator::RDG_SORT_LASTNAME;
119
                } else {
120
                    $sorting = ResultsDataGenerator::RDG_SORT_FIRSTNAME;
121
                }
122
                break;
123
                // Score
124
            case 2 + $col_adjust:
125
                $sorting = ResultsDataGenerator::RDG_SORT_SCORE;
126
                break;
127
            case 3 + $col_adjust:
128
                $sorting = ResultsDataGenerator::RDG_SORT_MASK;
129
                break;
130
        }
131
132
        if ('DESC' === $this->direction) {
133
            $sorting |= ResultsDataGenerator::RDG_SORT_DESC;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sorting does not seem to be defined for all execution paths leading up to this point.
Loading history...
134
        } else {
135
            $sorting |= ResultsDataGenerator::RDG_SORT_ASC;
136
        }
137
138
        $data_array = $this->datagen->get_data($sorting, $from, $this->per_page);
139
140
        $model = ExerciseLib::getCourseScoreModel();
141
142
        // generate the data to display
143
        $sortable_data = [];
144
        foreach ($data_array as $item) {
145
            $row = [];
146
            if ('1' == $this->iscourse) {
147
                $row[] = $item['result_id'];
148
            }
149
            if ($isWesternNameOrder) {
150
                $row[] = $item['firstname'];
151
                $row[] = $item['lastname'];
152
            } else {
153
                $row[] = $item['lastname'];
154
                $row[] = $item['firstname'];
155
            }
156
157
            if (empty($model)) {
158
                $row[] = $this->renderScoreCell($item);
159
            }
160
161
            if ($scoredisplay->is_custom()) {
162
                $row[] = $item['display'];
163
            }
164
            if (!$this->forprint) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->forprint of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
165
                $row[] = $this->build_edit_column($item);
166
            }
167
            $sortable_data[] = $row;
168
        }
169
170
        return $sortable_data;
171
    }
172
173
    private function renderScoreCell(array $item): string
174
    {
175
        $raw = $item['score'] ?? '';
176
        $text = html_entity_decode(strip_tags((string) $raw));
177
        $text = trim(preg_replace('/\s+/', ' ', $text));
178
179
        // Extract numeric parts from legacy strings like: "5 % (5 / 100)"
180
        preg_match_all('/\d+(?:[\\.,]\d+)?/', $text, $m);
181
        $numbers = $m[0] ?? [];
182
183
        $score = $numbers[0] ?? '';
184
        $max = !empty($numbers) ? end($numbers) : '';
185
186
        $score = str_replace(',', '.', (string) $score);
187
        $max = str_replace(',', '.', (string) $max);
188
189
        $percent = $item['percentage_score'] ?? null;
190
        $percentLabel = is_numeric($percent) ? api_number_format((float) $percent, 0) : '';
191
192
        // If we don't have a numeric score, just show the legacy text nicely.
193
        if ($score === '' && $text !== '') {
194
            return '<div class="max-w-xs text-body-2 text-gray-50 italic">'.Security::remove_XSS($text).'</div>';
195
        }
196
197
        $fraction = ($score !== '' && $max !== '') ? '('.$score.'/'.$max.')' : '';
198
        $left = $score !== '' ? Security::remove_XSS($score) : '—';
199
200
        // Choose semantic styling based on percentage.
201
        //  - success: >= 80
202
        //  - warning: 50-79
203
        //  - danger : < 50
204
        $toneBg = 'bg-support-1';
205
        $toneText = 'text-gray-90';
206
        $toneBarBg = 'bg-gray-20';
207
        $toneBarFill = 'bg-primary';
208
        $tonePillBg = 'bg-support-1';
209
        $tonePillText = 'text-support-4';
210
211
        if (is_numeric($percent)) {
212
            $p = (float) $percent;
213
214
            if ($p >= 80) {
215
                $toneBg = 'bg-support-2';
216
                $toneText = 'text-gray-90';
217
                $toneBarBg = 'bg-gray-20';
218
                $toneBarFill = 'bg-success';
219
                $tonePillBg = 'bg-support-2';
220
                $tonePillText = 'text-success';
221
            } elseif ($p >= 50) {
222
                $toneBg = 'bg-support-6';
223
                $toneText = 'text-gray-90';
224
                $toneBarBg = 'bg-gray-20';
225
                $toneBarFill = 'bg-warning';
226
                $tonePillBg = 'bg-support-6';
227
                $tonePillText = 'text-warning';
228
            } else {
229
                $toneBg = 'bg-support-6';
230
                $toneText = 'text-gray-90';
231
                $toneBarBg = 'bg-gray-20';
232
                $toneBarFill = 'bg-danger';
233
                $tonePillBg = 'bg-support-6';
234
                $tonePillText = 'text-danger';
235
            }
236
        }
237
238
        // Progress bar (uses theme colors)
239
        $bar = '';
240
        if (is_numeric($percent)) {
241
            $p = max(0, min(100, (float) $percent));
242
            $bar = '
243
            <div class="mt-2 h-2 w-full rounded-full '.$toneBarBg.' overflow-hidden">
244
                <div class="h-2 rounded-full '.$toneBarFill.'" style="width: '.$p.'%"></div>
245
            </div>
246
        ';
247
        }
248
249
        // Percent pill (uses theme colors)
250
        $pill = '';
251
        if ($percentLabel !== '') {
252
            $pill = '<span class="inline-flex items-center rounded-full '.$tonePillBg.' px-2 py-0.5 text-tiny '.$tonePillText.'">'
253
                .Security::remove_XSS($percentLabel).'%</span>';
254
        }
255
256
        $fractionHtml = '';
257
        if ($fraction !== '') {
258
            $fractionHtml = '<div class="mt-1 text-tiny text-gray-50">'.Security::remove_XSS($fraction).'</div>';
259
        }
260
261
        return '
262
        <div class="min-w-[10rem] max-w-xs rounded-xl border border-gray-25 px-3 py-2 '.$toneBg.'">
263
            <div class="flex items-center gap-2">
264
                <span class="font-semibold '.$toneText.'">'.$left.'</span>
265
                '.$pill.'
266
            </div>
267
            '.$fractionHtml.'
268
            '.$bar.'
269
        </div>
270
    ';
271
    }
272
273
    /**
274
     * @param Result $result
275
     * @param string $url
276
     *
277
     * @return string
278
     */
279
    public static function getResultAttemptTable($result, $url = '')
280
    {
281
        if (empty($result)) {
282
            return '';
283
        }
284
285
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT_ATTEMPT);
286
287
        $sql = "SELECT * FROM $table WHERE result_id = ".$result->get_id().' ORDER BY created_at DESC';
288
        $resultQuery = Database::query($sql);
289
        $list = Database::store_result($resultQuery);
290
291
        $htmlTable = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
292
        $htmlTable->setHeaderContents(0, 0, get_lang('Score'));
293
        $htmlTable->setHeaderContents(0, 1, get_lang('Comment'));
294
        $htmlTable->setHeaderContents(0, 2, get_lang('Created at'));
295
296
        if (!empty($url)) {
297
            $htmlTable->setHeaderContents(0, 3, get_lang('Detail'));
298
        }
299
300
        $row = 1;
301
        foreach ($list as $data) {
302
            $htmlTable->setCellContents($row, 0, $data['score']);
303
            $htmlTable->setCellContents($row, 1, $data['comment']);
304
            $htmlTable->setCellContents($row, 2, Display::dateToStringAgoAndLongDate($data['created_at']));
305
            if (!empty($url)) {
306
                $htmlTable->setCellContents(
307
                    $row,
308
                    3,
309
                    Display::url(
310
                        Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
311
                        $url.'&action=delete_attempt&result_attempt_id='.$data['id']
312
                    )
313
                );
314
            }
315
            $row++;
316
        }
317
318
        return $htmlTable->toHtml();
319
    }
320
321
    /**
322
     * @param array $item
323
     *
324
     * @return string
325
     */
326
    private function build_edit_column($item)
327
    {
328
        $locked_status = $this->evaluation->get_locked();
329
        $allowMultipleAttempts = ('true' === api_get_setting('gradebook.gradebook_multiple_evaluation_attempts'));
330
        $baseUrl = api_get_self().'?selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq();
331
        $editColumn = '';
332
        if (api_is_allowed_to_edit(null, true) && 0 == $locked_status) {
333
            if ($allowMultipleAttempts) {
334
                if (!empty($item['percentage_score'])) {
335
                    $editColumn .=
336
                        Display::url(
337
                            Display::getMdiIcon(ActionIcon::ADD, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Add attempt')),
338
                            $baseUrl.'&action=add_attempt&editres='.$item['result_id']
339
                        );
340
                } else {
341
                    $editColumn .= '<a href="'.api_get_self().'?editres='.$item['result_id'].'&selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq().'">'.
342
                        Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')).'</a>';
343
                }
344
            } else {
345
                $editColumn .= '<a href="'.api_get_self().'?editres='.$item['result_id'].'&selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq().'">'.
346
                    Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')).'</a>';
347
            }
348
            $editColumn .= ' <a href="'.api_get_self().'?delete_mark='.$item['result_id'].'&selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq().'">'.
349
                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')).'</a>';
350
        }
351
352
        if (null == $this->evaluation->getCourseId()) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $this->evaluation->getCourseId() of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
353
            $editColumn .= '&nbsp;<a href="'.api_get_self().'?resultdelete='.$item['result_id'].'&selecteval='.$this->evaluation->get_id().'" onclick="return confirmationuser();">';
354
            $editColumn .= Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete'));
355
            $editColumn .= '</a>';
356
            $editColumn .= '&nbsp;<a href="user_stats.php?userid='.$item['id'].'&selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq().'">';
357
            $editColumn .= Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Statistics'));
358
            $editColumn .= '</a>';
359
        }
360
361
        // Evaluation's origin is a link
362
        if ($this->evaluation->get_category_id() < 0) {
363
            $link = LinkFactory::get_evaluation_link($this->evaluation->get_id());
364
            $doc_url = $link->get_view_url($item['id']);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $doc_url is correct as $link->get_view_url($item['id']) targeting AbstractLink::get_view_url() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
365
366
            if (null != $doc_url) {
367
                $editColumn .= '&nbsp;<a href="'.$doc_url.'" target="_blank">';
368
                $editColumn .= Display::getMdiIcon(ToolIcon::LINK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Open document')).'</a>';
369
            }
370
        }
371
372
        return $editColumn;
373
    }
374
}
375