<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for representing a column.
*
* @package core_question
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* Base class for representing a column.
*
* @copyright 2009 Tim Hunt
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
< abstract class column_base {
> abstract class column_base extends view_component {
>
> /**
> * @var string A separator for joining column attributes together into a unique ID string.
> */
> const ID_SEPARATOR = '-';
/**
* @var view $qbank the question bank view we are helping to render.
*/
protected $qbank;
/** @var bool determine whether the column is td or th. */
protected $isheading = false;
< /**
< * Constructor.
< * @param view $qbank the question bank view we are helping to render.
< */
< public function __construct(view $qbank) {
< $this->qbank = $qbank;
< $this->init();
< }
> /** @var bool determine whether the column is visible */
> public $isvisible = true;
/**
< * A chance for subclasses to initialise themselves, for example to load lang strings,
< * without having to override the constructor.
> * Return an instance of this column, based on the column name.
> *
> * In the case of the base class, we don't actually use the column name since the class represents one specific column.
> * However, sub-classes may use the column name as an additional constructor to the parameter.
> *
> * @param view $view Question bank view
> * @param string $columnname The column name for this instance, as returned by {@see get_column_name()}
> * @return column_base An instance of this class.
*/
< protected function init(): void {
> public static function from_column_name(view $view, string $columnname): column_base {
> return new static($view);
}
/**
* Set the column as heading
*/
public function set_as_heading(): void {
$this->isheading = true;
}
/**
* Check if the column is an extra row of not.
*/
public function is_extra_row(): bool {
return false;
}
/**
* Check if the row has an extra preference to view/hide.
*/
public function has_preference(): bool {
return false;
}
/**
* Get if the preference key of the row.
*/
public function get_preference_key(): string {
return '';
}
/**
* Get if the preference of the row.
*/
public function get_preference(): bool {
return false;
}
/**
* Output the column header cell.
> *
*/
> * @param column_action_base[] $columnactions A list of column actions to include in the header.
public function display_header(): void {
> * @param string $width A CSS width property value.
< public function display_header(): void {
> public function display_header(array $columnactions = [], string $width = ''): void {
$renderer = $PAGE->get_renderer('core_question', 'bank');
$data = [];
$data['sortable'] = true;
$data['extraclasses'] = $this->get_classes();
$sortable = $this->is_sortable();
< $name = get_class($this);
> $name = str_replace('\\', '__', get_class($this));
$title = $this->get_title();
$tip = $this->get_title_tip();
$links = [];
if (is_array($sortable)) {
if ($title) {
$data['title'] = $title;
}
foreach ($sortable as $subsort => $details) {
$links[] = $this->make_sort_link($name . '-' . $subsort,
$details['title'], isset($details['tip']) ? $details['tip'] : '', !empty($details['reverse']));
}
$data['sortlinks'] = implode(' / ', $links);
} else if ($sortable) {
$data['sortlinks'] = $this->make_sort_link($name, $title, $tip);
} else {
$data['sortable'] = false;
$data['tiptitle'] = $title;
if ($tip) {
$data['sorttip'] = true;
$data['tip'] = $tip;
}
}
$help = $this->help_icon();
if ($help) {
$data['help'] = $help->export_for_template($renderer);
}
> $data['colname'] = $this->get_column_name();
echo $renderer->render_column_header($data);
> $data['columnid'] = $this->get_column_id();
}
> $data['name'] = $title;
> $data['class'] = $name;
/**
> $data['width'] = $width;
* Title for this column. Not used if is_sortable returns an array.
> if (!empty($columnactions)) {
*/
> $actions = array_map(fn($columnaction) => $columnaction->get_action_menu_link($this), $columnactions);
abstract public function get_title();
> $actionmenu = new \action_menu($actions);
> $data['actionmenu'] = $actionmenu->export_for_template($renderer);
/**
> }
* Use this when get_title() returns
>
* something very short, and you want a longer version as a tool tip.
*
* @return string a fuller version of the name.
*/
public function get_title_tip() {
return '';
}
/**
* If you return a help icon here, it is shown in the column header after the title.
*
* @return \help_icon|null help icon to show, if required.
*/
public function help_icon(): ?\help_icon {
return null;
}
/**
* Get a link that changes the sort order, and indicates the current sort state.
< * @param string $sort the column to sort on.
> *
> * @param string $sortname the column to sort on.
* @param string $title the link text.
* @param string $tip the link tool-tip text. If empty, defaults to title.
* @param bool $defaultreverse whether the default sort order for this column is descending, rather than ascending.
* @return string
*/
< protected function make_sort_link($sort, $title, $tip, $defaultreverse = false): string {
> protected function make_sort_link($sortname, $title, $tip, $defaultreverse = false): string {
global $PAGE;
$sortdata = [];
< $currentsort = $this->qbank->get_primary_sort_order($sort);
> $currentsort = $this->qbank->get_primary_sort_order($sortname);
$newsortreverse = $defaultreverse;
if ($currentsort) {
< $newsortreverse = $currentsort > 0;
> $newsortreverse = $currentsort == SORT_ASC;
}
if (!$tip) {
$tip = $title;
}
if ($newsortreverse) {
$tip = get_string('sortbyxreverse', '', $tip);
} else {
$tip = get_string('sortbyx', '', $tip);
}
$link = $title;
if ($currentsort) {
< $link .= $this->get_sort_icon($currentsort < 0);
> $link .= $this->get_sort_icon($currentsort == SORT_DESC);
}
< $sortdata['sorturl'] = $this->qbank->new_sort_url($sort, $newsortreverse);
> $sortdata['sorturl'] = $this->qbank->new_sort_url($sortname, $newsortreverse);
> $sortdata['sortname'] = $sortname;
$sortdata['sortcontent'] = $link;
$sortdata['sorttip'] = $tip;
> $sortdata['sortorder'] = $newsortreverse ? SORT_DESC : SORT_ASC;
$renderer = $PAGE->get_renderer('core_question', 'bank');
return $renderer->render_column_sort($sortdata);
}
/**
* Get an icon representing the corrent sort state.
* @param bool $reverse sort is descending, not ascending.
* @return string HTML image tag.
*/
protected function get_sort_icon($reverse): string {
global $OUTPUT;
if ($reverse) {
return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'), '', ['class' => 'iconsort']);
} else {
return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'), '', ['class' => 'iconsort']);
}
}
/**
* Output this column.
* @param object $question the row from the $question table, augmented with extra information.
* @param string $rowclasses CSS class names that should be applied to this row of output.
*/
public function display($question, $rowclasses): void {
$this->display_start($question, $rowclasses);
$this->display_content($question, $rowclasses);
$this->display_end($question, $rowclasses);
}
/**
* Output the opening column tag. If it is set as heading, it will use <th> tag instead of <td>
*
* @param \stdClass $question
* @param string $rowclasses
*/
protected function display_start($question, $rowclasses): void {
$tag = 'td';
< $attr = ['class' => $this->get_classes()];
> $attr = [
> 'class' => $this->get_classes(),
> 'data-columnid' => $this->get_column_id(),
> ];
if ($this->isheading) {
$tag = 'th';
$attr['scope'] = 'row';
}
echo \html_writer::start_tag($tag, $attr);
}
/**
* The CSS classes to apply to every cell in this column.
*
* @return string
*/
protected function get_classes(): string {
$classes = $this->get_extra_classes();
$classes[] = $this->get_name();
return implode(' ', $classes);
}
/**
* Get the internal name for this column. Used as a CSS class name,
* and to store information about the current sort. Must match PARAM_ALPHA.
*
* @return string column name.
*/
abstract public function get_name();
/**
* Get the name of this column. This must be unique.
* When using the inherited class to make many columns from one parent,
* ensure each instance returns a unique value.
*
* @return string The unique name;
*/
public function get_column_name() {
return (new \ReflectionClass($this))->getShortName();
}
/**
> * Return a unique ID for this column object.
* Any extra class names you would like applied to every cell in this column.
> *
*
> * This is constructed using the class name and get_column_name(), which must be unique.
* @return array
> *
*/
> * The combination of these attributes allows the object to be reconstructed, by splitting the ID into its constituent
public function get_extra_classes(): array {
> * parts then calling {@see from_column_name()}, like this:
return [];
> * [$class, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2);
}
> * $column = $class::from_column_name($qbank, $columnname);
> * Including 2 as the $limit parameter for explode() is a good idea for safely, in case a plugin defines a column with the
/**
> * ID_SEPARATOR in the column name.
* Output the contents of this column.
> *
* @param object $question the row from the $question table, augmented with extra information.
> * @return string The column ID.
* @param string $rowclasses CSS class names that should be applied to this row of output.
> */
*/
> final public function get_column_id(): string {
abstract protected function display_content($question, $rowclasses);
> return implode(self::ID_SEPARATOR, [static::class, $this->get_column_name()]);
> }
/**
>
* Output the closing column tag
> /**
*
> * Return the default column width in pixels.
* @param object $question
> *
* @param string $rowclasses
> * @return int
*/
> */
protected function display_end($question, $rowclasses): void {
> public function get_default_width(): int {
$tag = 'td';
> return 120;
if ($this->isheading) {
> }
$tag = 'th';
>
}
> /**
echo \html_writer::end_tag($tag);
}
< /**
< * Return an array 'table_alias' => 'JOIN clause' to bring in any data that
< * this column required.
< *
< * The return values for all the columns will be checked. It is OK if two
< * columns join in the same table with the same alias and identical JOIN clauses.
< * If to columns try to use the same alias with different joins, you get an error.
< * The only table included by default is the question table, which is aliased to 'q'.
< *
< * It is importnat that your join simply adds additional data (or NULLs) to the
< * existing rows of the query. It must not cause additional rows.
< *
< * @return array 'table_alias' => 'JOIN clause'
< */
public function get_extra_joins(): array {
return [];
}
< /**
< * Use table alias 'q' for the question table, or one of the
< * ones from get_extra_joins. Every field requested must specify a table prefix.
< *
< * @return array fields required.
< */
public function get_required_fields(): array {
return [];
}
/**
* If this column requires any aggregated statistics, it should declare that here.
*
* This is those statistics can be efficiently loaded in bulk.
*
* The statistics are all loaded just before load_additional_data is called on each column.
* The values are then available from $this->qbank->get_aggregate_statistic(...);
*
* @return string[] the names of the required statistics fields. E.g. ['facility'].
*/
public function get_required_statistics_fields(): array {
return [];
}
/**
* If this column needs extra data (e.g. tags) then load that here.
*
* The extra data should be added to the question object in the array.
* Probably a good idea to check that another column has not already
* loaded the data you want.
*
* @param \stdClass[] $questions the questions that will be displayed, indexed by question id.
*/
public function load_additional_data(array $questions) {
}
/**
* Load the tags for each question.
*
* Helper that can be used from {@see load_additional_data()};
*
* @param array $questions
*/
public function load_question_tags(array $questions): void {
$firstquestion = reset($questions);
if (isset($firstquestion->tags)) {
// Looks like tags are already loaded, so don't do it again.
return;
}
// Load the tags.
$tagdata = \core_tag_tag::get_items_tags('core_question', 'question',
array_keys($questions));
// Add them to the question objects.
foreach ($tagdata as $questionid => $tags) {
$questions[$questionid]->tags = $tags;
}
}
/**
* Can this column be sorted on? You can return either:
* + false for no (the default),
* + a field name, if sorting this column corresponds to sorting on that datbase field.
* + an array of subnames to sort on as follows
* return [
* 'firstname' => ['field' => 'uc.firstname', 'title' => get_string('firstname')],
* 'lastname' => ['field' => 'uc.lastname', 'title' => get_string('lastname')],
* ];
* As well as field, and field, you can also add 'revers' => 1 if you want the default sort
* order to be DESC.
* @return mixed as above.
*/
public function is_sortable() {
return false;
}
/**
* Helper method for building sort clauses.
* @param bool $reverse whether the normal direction should be reversed.
* @return string 'ASC' or 'DESC'
*/
protected function sortorder($reverse): string {
if ($reverse) {
return ' DESC';
} else {
return ' ASC';
}
}
/**
* Sorts the expressions.
*
* @param bool $reverse Whether to sort in the reverse of the default sort order.
* @param string $subsort if is_sortable returns an array of subnames, then this will be
* one of those. Otherwise will be empty.
* @return string some SQL to go in the order by clause.
*/
public function sort_expression($reverse, $subsort): string {
$sortable = $this->is_sortable();
if (is_array($sortable)) {
if (array_key_exists($subsort, $sortable)) {
return $sortable[$subsort]['field'] . $this->sortorder($reverse);
} else {
throw new \coding_exception('Unexpected $subsort type: ' . $subsort);
}
} else if ($sortable) {
return $sortable . $this->sortorder($reverse);
} else {
throw new \coding_exception('sort_expression called on a non-sortable column.');
}
}
> /**
}
> * Output the column with an example value.
> *
> * By default, this will call $this->display() using whatever dummy data is passed in. Columns can override this
> * to provide example output without requiring valid data.
> *
> * @param \stdClass $question the row from the $question table, augmented with extra information.
> * @param string $rowclasses CSS class names that should be applied to this row of output.
> */
> public function display_preview(\stdClass $question, string $rowclasses): void {
> $this->display($question, $rowclasses);
> }