MDL-62814 Question: Improve column base display_header
[moodle.git] / question / classes / bank / column_base.php
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/>.
17 /**
18  *
19  * @package    moodlecore
20  * @subpackage questionbank
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  */
26 namespace core_question\bank;
28 /**
29  * Base class for representing a column in a {@link question_bank_view}.
30  *
31  * @copyright  2009 Tim Hunt
32  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33  */
35 abstract class column_base {
36     /**
37      * @var question_bank_view
38      */
39     protected $qbank;
41     /** @var bool determine whether the column is td or th. */
42     protected $isheading = false;
44     /**
45      * Constructor.
46      * @param $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     }
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     }
60     /**
61      * Set the column as heading
62      */
63     public function set_as_heading() {
64         $this->isheading = true;
65     }
67     public function is_extra_row() {
68         return false;
69     }
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     }
104     /**
105      * Title for this column. Not used if is_sortable returns an array.
106      * @param object $question the row from the $question table, augmented with extra information.
107      * @param string $rowclasses CSS class names that should be applied to this row of output.
108      */
109     protected abstract function get_title();
111     /**
112      * @return string a fuller version of the name. Use this when get_title() returns
113      * something very short, and you want a longer version as a tool tip.
114      */
115     protected function get_title_tip() {
116         return '';
117     }
119     /**
120      * Get a link that changes the sort order, and indicates the current sort state.
121      * @param $name internal name used for this type of sorting.
122      * @param $currentsort the current sort order -1, 0, 1 for descending, none, ascending.
123      * @param $title the link text.
124      * @param $defaultreverse whether the default sort order for this column is descending, rather than ascending.
125      * @return string HTML fragment.
126      */
127     protected function make_sort_link($sort, $title, $tip, $defaultreverse = false) {
128         $currentsort = $this->qbank->get_primary_sort_order($sort);
129         $newsortreverse = $defaultreverse;
130         if ($currentsort) {
131             $newsortreverse = $currentsort > 0;
132         }
133         if (!$tip) {
134             $tip = $title;
135         }
136         if ($newsortreverse) {
137             $tip = get_string('sortbyxreverse', '', $tip);
138         } else {
139             $tip = get_string('sortbyx', '', $tip);
140         }
141         $link = '<a href="' . $this->qbank->new_sort_url($sort, $newsortreverse) . '" title="' . $tip . '">';
142         $link .= $title;
143         if ($currentsort) {
144             $link .= $this->get_sort_icon($currentsort < 0);
145         }
146         $link .= '</a>';
147         return $link;
148     }
150     /**
151      * Get an icon representing the corrent sort state.
152      * @param $reverse sort is descending, not ascending.
153      * @return string HTML image tag.
154      */
155     protected function get_sort_icon($reverse) {
156         global $OUTPUT;
157         if ($reverse) {
158             return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'), '', array('class' => 'iconsort'));
159         } else {
160             return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'), '', array('class' => 'iconsort'));
161         }
162     }
164     /**
165      * Output this column.
166      * @param object $question the row from the $question table, augmented with extra information.
167      * @param string $rowclasses CSS class names that should be applied to this row of output.
168      */
169     public function display($question, $rowclasses) {
170         $this->display_start($question, $rowclasses);
171         $this->display_content($question, $rowclasses);
172         $this->display_end($question, $rowclasses);
173     }
175     /**
176      * Output the opening column tag.  If it is set as heading, it will use <th> tag instead of <td>
177      *
178      * @param stdClass $question
179      * @param array $rowclasses
180      */
181     protected function display_start($question, $rowclasses) {
182         $tag = 'td';
183         $attr = array('class' => $this->get_classes());
184         if ($this->isheading) {
185             $tag = 'th';
186             $attr['scope'] = 'row';
187         }
188         echo \html_writer::start_tag($tag, $attr);
189     }
191     /**
192      * @return string the CSS classes to apply to every cell in this column.
193      */
194     protected function get_classes() {
195         $classes = $this->get_extra_classes();
196         $classes[] = $this->get_name();
197         return implode(' ', $classes);
198     }
200     /**
201      * @param object $question the row from the $question table, augmented with extra information.
202      * @return string internal name for this column. Used as a CSS class name,
203      *     and to store information about the current sort. Must match PARAM_ALPHA.
204      */
205     public abstract function get_name();
207     /**
208      * @return array any extra class names you would like applied to every cell in this column.
209      */
210     public function get_extra_classes() {
211         return array();
212     }
214     /**
215      * Output the contents of this column.
216      * @param object $question the row from the $question table, augmented with extra information.
217      * @param string $rowclasses CSS class names that should be applied to this row of output.
218      */
219     protected abstract function display_content($question, $rowclasses);
221     /**
222      * Output the closing column tag
223      *
224      * @param object $question
225      * @param string $rowclasses
226      */
227     protected function display_end($question, $rowclasses) {
228         $tag = 'td';
229         if ($this->isheading) {
230             $tag = 'th';
231         }
232         echo \html_writer::end_tag($tag);
233     }
235     /**
236      * Return an array 'table_alias' => 'JOIN clause' to bring in any data that
237      * this column required.
238      *
239      * The return values for all the columns will be checked. It is OK if two
240      * columns join in the same table with the same alias and identical JOIN clauses.
241      * If to columns try to use the same alias with different joins, you get an error.
242      * The only table included by default is the question table, which is aliased to 'q'.
243      *
244      * It is importnat that your join simply adds additional data (or NULLs) to the
245      * existing rows of the query. It must not cause additional rows.
246      *
247      * @return array 'table_alias' => 'JOIN clause'
248      */
249     public function get_extra_joins() {
250         return array();
251     }
253     /**
254      * @return array fields required. use table alias 'q' for the question table, or one of the
255      * ones from get_extra_joins. Every field requested must specify a table prefix.
256      */
257     public function get_required_fields() {
258         return array();
259     }
261     /**
262      * Can this column be sorted on? You can return either:
263      *  + false for no (the default),
264      *  + a field name, if sorting this column corresponds to sorting on that datbase field.
265      *  + an array of subnames to sort on as follows
266      *  return array(
267      *      'firstname' => array('field' => 'uc.firstname', 'title' => get_string('firstname')),
268      *      'lastname' => array('field' => 'uc.lastname', 'field' => get_string('lastname')),
269      *  );
270      * As well as field, and field, you can also add 'revers' => 1 if you want the default sort
271      * order to be DESC.
272      * @return mixed as above.
273      */
274     public function is_sortable() {
275         return false;
276     }
278     /**
279      * Helper method for building sort clauses.
280      * @param bool $reverse whether the normal direction should be reversed.
281      * @param string $normaldir 'ASC' or 'DESC'
282      * @return string 'ASC' or 'DESC'
283      */
284     protected function sortorder($reverse) {
285         if ($reverse) {
286             return ' DESC';
287         } else {
288             return ' ASC';
289         }
290     }
292     /**
293      * @param $reverse Whether to sort in the reverse of the default sort order.
294      * @param $subsort if is_sortable returns an array of subnames, then this will be
295      *      one of those. Otherwise will be empty.
296      * @return string some SQL to go in the order by clause.
297      */
298     public function sort_expression($reverse, $subsort) {
299         $sortable = $this->is_sortable();
300         if (is_array($sortable)) {
301             if (array_key_exists($subsort, $sortable)) {
302                 return $sortable[$subsort]['field'] . $this->sortorder($reverse, !empty($sortable[$subsort]['reverse']));
303             } else {
304                 throw new coding_exception('Unexpected $subsort type: ' . $subsort);
305             }
306         } else if ($sortable) {
307             return $sortable . $this->sortorder($reverse);
308         } else {
309             throw new coding_exception('sort_expression called on a non-sortable column.');
310         }
311     }