1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Base class for representing a column in a {@link question_bank_view}.
19 *
20 * @package   core_question
21 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
22 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace core_question\bank;
26defined('MOODLE_INTERNAL') || die();
27
28
29/**
30 * Base class for representing a column in a {@link question_bank_view}.
31 *
32 * @copyright 2009 Tim Hunt
33 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35abstract class column_base {
36    /**
37     * @var view $qbank the question bank view we are helping to render.
38     */
39    protected $qbank;
40
41    /** @var bool determine whether the column is td or th. */
42    protected $isheading = false;
43
44    /**
45     * Constructor.
46     * @param view $qbank the question bank view we are helping to render.
47     */
48    public function __construct(view $qbank) {
49        $this->qbank = $qbank;
50        $this->init();
51    }
52
53    /**
54     * A chance for subclasses to initialise themselves, for example to load lang strings,
55     * without having to override the constructor.
56     */
57    protected function init() {
58    }
59
60    /**
61     * Set the column as heading
62     */
63    public function set_as_heading() {
64        $this->isheading = true;
65    }
66
67    public function is_extra_row() {
68        return false;
69    }
70
71    /**
72     * Output the column header cell.
73     */
74    public function display_header() {
75        echo '<th class="header ' . $this->get_classes() . '" scope="col">';
76        $sortable = $this->is_sortable();
77        $name = get_class($this);
78        $title = $this->get_title();
79        $tip = $this->get_title_tip();
80        if (is_array($sortable)) {
81            if ($title) {
82                echo '<div class="title">' . $title . '</div>';
83            }
84            $links = array();
85            foreach ($sortable as $subsort => $details) {
86                $links[] = $this->make_sort_link($name . '-' . $subsort,
87                        $details['title'], isset($details['tip']) ? $details['tip'] : '', !empty($details['reverse']));
88            }
89            echo '<div class="sorters">' . implode(' / ', $links) . '</div>';
90        } else if ($sortable) {
91            echo $this->make_sort_link($name, $title, $tip);
92        } else {
93            if ($tip) {
94                echo '<span title="' . $tip . '">';
95            }
96            echo $title;
97            if ($tip) {
98                echo '</span>';
99            }
100        }
101        echo "</th>\n";
102    }
103
104    /**
105     * Title for this column. Not used if is_sortable returns an array.
106     */
107    protected abstract function get_title();
108
109    /**
110     * @return string a fuller version of the name. Use this when get_title() returns
111     * something very short, and you want a longer version as a tool tip.
112     */
113    protected function get_title_tip() {
114        return '';
115    }
116
117    /**
118     * Get a link that changes the sort order, and indicates the current sort state.
119     * @param string $sort the column to sort on.
120     * @param string $title the link text.
121     * @param string $tip the link tool-tip text. If empty, defaults to title.
122     * @param bool $defaultreverse whether the default sort order for this column is descending, rather than ascending.
123     * @return string HTML fragment.
124     */
125    protected function make_sort_link($sort, $title, $tip, $defaultreverse = false) {
126        $currentsort = $this->qbank->get_primary_sort_order($sort);
127        $newsortreverse = $defaultreverse;
128        if ($currentsort) {
129            $newsortreverse = $currentsort > 0;
130        }
131        if (!$tip) {
132            $tip = $title;
133        }
134        if ($newsortreverse) {
135            $tip = get_string('sortbyxreverse', '', $tip);
136        } else {
137            $tip = get_string('sortbyx', '', $tip);
138        }
139        $link = '<a href="' . $this->qbank->new_sort_url($sort, $newsortreverse) . '" title="' . $tip . '">';
140        $link .= $title;
141        if ($currentsort) {
142            $link .= $this->get_sort_icon($currentsort < 0);
143        }
144        $link .= '</a>';
145        return $link;
146    }
147
148    /**
149     * Get an icon representing the corrent sort state.
150     * @param bool $reverse sort is descending, not ascending.
151     * @return string HTML image tag.
152     */
153    protected function get_sort_icon($reverse) {
154        global $OUTPUT;
155        if ($reverse) {
156            return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'), '', array('class' => 'iconsort'));
157        } else {
158            return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'), '', array('class' => 'iconsort'));
159        }
160    }
161
162    /**
163     * Output this column.
164     * @param object $question the row from the $question table, augmented with extra information.
165     * @param string $rowclasses CSS class names that should be applied to this row of output.
166     */
167    public function display($question, $rowclasses) {
168        $this->display_start($question, $rowclasses);
169        $this->display_content($question, $rowclasses);
170        $this->display_end($question, $rowclasses);
171    }
172
173    /**
174     * Output the opening column tag.  If it is set as heading, it will use <th> tag instead of <td>
175     *
176     * @param \stdClass $question
177     * @param string $rowclasses
178     */
179    protected function display_start($question, $rowclasses) {
180        $tag = 'td';
181        $attr = array('class' => $this->get_classes());
182        if ($this->isheading) {
183            $tag = 'th';
184            $attr['scope'] = 'row';
185        }
186        echo \html_writer::start_tag($tag, $attr);
187    }
188
189    /**
190     * @return string the CSS classes to apply to every cell in this column.
191     */
192    protected function get_classes() {
193        $classes = $this->get_extra_classes();
194        $classes[] = $this->get_name();
195        return implode(' ', $classes);
196    }
197
198    /**
199     * Get the internal name for this column. Used as a CSS class name,
200     * and to store information about the current sort. Must match PARAM_ALPHA.
201     *
202     * @return string column name.
203     */
204    public abstract function get_name();
205
206    /**
207     * @return array any extra class names you would like applied to every cell in this column.
208     */
209    public function get_extra_classes() {
210        return array();
211    }
212
213    /**
214     * Output the contents of this column.
215     * @param object $question the row from the $question table, augmented with extra information.
216     * @param string $rowclasses CSS class names that should be applied to this row of output.
217     */
218    protected abstract function display_content($question, $rowclasses);
219
220    /**
221     * Output the closing column tag
222     *
223     * @param object $question
224     * @param string $rowclasses
225     */
226    protected function display_end($question, $rowclasses) {
227        $tag = 'td';
228        if ($this->isheading) {
229            $tag = 'th';
230        }
231        echo \html_writer::end_tag($tag);
232    }
233
234    /**
235     * Return an array 'table_alias' => 'JOIN clause' to bring in any data that
236     * this column required.
237     *
238     * The return values for all the columns will be checked. It is OK if two
239     * columns join in the same table with the same alias and identical JOIN clauses.
240     * If to columns try to use the same alias with different joins, you get an error.
241     * The only table included by default is the question table, which is aliased to 'q'.
242     *
243     * It is importnat that your join simply adds additional data (or NULLs) to the
244     * existing rows of the query. It must not cause additional rows.
245     *
246     * @return array 'table_alias' => 'JOIN clause'
247     */
248    public function get_extra_joins() {
249        return array();
250    }
251
252    /**
253     * @return array fields required. use table alias 'q' for the question table, or one of the
254     * ones from get_extra_joins. Every field requested must specify a table prefix.
255     */
256    public function get_required_fields() {
257        return array();
258    }
259
260    /**
261     * If this column needs extra data (e.g. tags) then load that here.
262     *
263     * The extra data should be added to the question object in the array.
264     * Probably a good idea to check that another column has not already
265     * loaded the data you want.
266     *
267     * @param \stdClass[] $questions the questions that will be displayed.
268     */
269    public function load_additional_data(array $questions) {
270    }
271
272    /**
273     * Load the tags for each question.
274     *
275     * Helper that can be used from {@link load_additional_data()};
276     *
277     * @param array $questions
278     */
279    public function load_question_tags(array $questions) {
280        $firstquestion = reset($questions);
281        if (isset($firstquestion->tags)) {
282            // Looks like tags are already loaded, so don't do it again.
283            return;
284        }
285
286        // Load the tags.
287        $tagdata = \core_tag_tag::get_items_tags('core_question', 'question',
288                array_keys($questions));
289
290        // Add them to the question objects.
291        foreach ($tagdata as $questionid => $tags) {
292            $questions[$questionid]->tags = $tags;
293        }
294    }
295
296    /**
297     * Can this column be sorted on? You can return either:
298     *  + false for no (the default),
299     *  + a field name, if sorting this column corresponds to sorting on that datbase field.
300     *  + an array of subnames to sort on as follows
301     *  return array(
302     *      'firstname' => array('field' => 'uc.firstname', 'title' => get_string('firstname')),
303     *      'lastname' => array('field' => 'uc.lastname', 'title' => get_string('lastname')),
304     *  );
305     * As well as field, and field, you can also add 'revers' => 1 if you want the default sort
306     * order to be DESC.
307     * @return mixed as above.
308     */
309    public function is_sortable() {
310        return false;
311    }
312
313    /**
314     * Helper method for building sort clauses.
315     * @param bool $reverse whether the normal direction should be reversed.
316     * @return string 'ASC' or 'DESC'
317     */
318    protected function sortorder($reverse) {
319        if ($reverse) {
320            return ' DESC';
321        } else {
322            return ' ASC';
323        }
324    }
325
326    /**
327     * @param bool $reverse Whether to sort in the reverse of the default sort order.
328     * @param string $subsort if is_sortable returns an array of subnames, then this will be
329     *      one of those. Otherwise will be empty.
330     * @return string some SQL to go in the order by clause.
331     */
332    public function sort_expression($reverse, $subsort) {
333        $sortable = $this->is_sortable();
334        if (is_array($sortable)) {
335            if (array_key_exists($subsort, $sortable)) {
336                return $sortable[$subsort]['field'] . $this->sortorder($reverse);
337            } else {
338                throw new \coding_exception('Unexpected $subsort type: ' . $subsort);
339            }
340        } else if ($sortable) {
341            return $sortable . $this->sortorder($reverse);
342        } else {
343            throw new \coding_exception('sort_expression called on a non-sortable column.');
344        }
345    }
346}
347