first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,93 @@
<?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/>.
/**
* A base class for actions that are an icon that lets you manipulate the question in some way.
*
* @package core_question
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* A base class for actions that are an icon that lets you manipulate the question in some way.
*
* @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
* @deprecated Since Moodle 4.3 MDL-75125 - Use question_action_base instead.
* @todo MDL-78090 This class will be deleted in Moodle 4.7
*/
abstract class action_column_base extends column_base {
/**
* @return string
* @deprecated Since Moodle 4.3
*/
public function get_title(): string {
debugging('The action_column_base class is deprecated. Please use question_action_base instead.', DEBUG_DEVELOPER);
return '&#160;';
}
/**
* @return string[]
* @deprecated Since Moodle 4.3
*/
public function get_extra_classes(): array {
debugging('The action_column_base class is deprecated. Please use question_action_base instead.', DEBUG_DEVELOPER);
return ['iconcol'];
}
/**
* @param $icon
* @param $title
* @param $url
* @return void
* @deprecated Since Moodle 4.3
*/
protected function print_icon($icon, $title, $url): void {
debugging('The action_column_base class is deprecated. Please use question_action_base instead.', DEBUG_DEVELOPER);
global $OUTPUT;
echo \html_writer::tag('a', $OUTPUT->pix_icon($icon, $title), ['title' => $title, 'href' => $url]);
}
/**
* @return string[]
* @deprecated Since Moodle 4.3
*/
public function get_extra_joins(): array {
debugging('The action_column_base class is deprecated. Please use question_action_base instead.', DEBUG_DEVELOPER);
return ['qv' => 'JOIN {question_versions} qv ON qv.questionid = q.id',
'qbe' => 'JOIN {question_bank_entries} qbe on qbe.id = qv.questionbankentryid',
'qc' => 'JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid'];
}
/**
* @return string[]
* @deprecated Since Moodle 4.3
*/
public function get_required_fields(): array {
debugging('The action_column_base class is deprecated. Please use question_action_base instead.', DEBUG_DEVELOPER);
// Createdby is required for permission checks.
// Qtype so we can easily avoid applying actions to question types that
// are no longer installed.
return ['q.id', 'q.qtype', 'q.createdby', 'qc.contextid'];
}
}
@@ -0,0 +1,78 @@
<?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/>.
namespace core_question\local\bank;
/**
* Class bulk_action_base is the base class for bulk actions ui.
*
* Every plugin wants to implement a bulk action, should extend this class, add appropriate values to the methods
* and finally pass this object via plugin_feature class.
*
* @package core_question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class bulk_action_base {
/**
* Title of the bulk action.
* Every bulk action will have a string to show in the list.
*
* @return string
*/
abstract public function get_bulk_action_title(): string;
/**
* A unique key for the bulk action, this will be used in the api to identify the action data.
* Every bulk must have a unique key to perform the action as a part of the form post in the base view.
* When questions are selected, it will post according to the key its selected from the dropdown.
*
* @return string
*/
abstract function get_key(): string;
/**
* URL of the bulk action redirect page.
* Bulk action can be performed by redirecting to a page and doing the appropriate selection
* and finally doing the action. The url will be url of the page where users will be redirected to
* select what to do with the selected questions.
*
* @return \moodle_url
*/
abstract public function get_bulk_action_url(): \moodle_url;
/**
* Get the capabilities for the bulk action.
* The bulk actions might have some capabilities to action them as a user.
* This method helps to get those caps which will be used by the base view before actioning the bulk action.
* For ex: ['moodle/question:moveall', 'moodle/question:add']
* At least one of the cap need to be true for the user to use this action.
*
* @return array|null
*/
public function get_bulk_action_capabilities(): ?array {
return null;
}
/**
* @deprecated since Moodle 4.0
*/
public function get_bulk_action_key() {
throw new \coding_exception(__FUNCTION__ . '() has been removed.');
}
}
@@ -0,0 +1,102 @@
<?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/>.
/**
* A column with a checkbox for each question with name q{questionid}.
*
* @package core_question
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
use core\output\checkbox_toggleall;
/**
* A column with a checkbox for each question with name q{questionid}.
*
* @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
*/
class checkbox_column extends column_base {
public function get_name(): string {
return 'checkbox';
}
public function get_title() {
global $OUTPUT;
$mastercheckbox = new checkbox_toggleall('qbank', true, [
'id' => 'qbheadercheckbox',
'name' => 'qbheadercheckbox',
'value' => '1',
'label' => get_string('selectall'),
'labelclasses' => 'accesshide',
]);
return $OUTPUT->render($mastercheckbox);
}
public function get_title_tip() {
return get_string('selectquestionsforbulk', 'question');
}
public function display_header(array $columnactions = [], string $width = ''): void {
global $PAGE;
$renderer = $PAGE->get_renderer('core_question', 'bank');
$data = [];
$data['sortable'] = false;
$data['extraclasses'] = $this->get_classes();
$name = get_class($this);
$data['sorttip'] = true;
$data['tiptitle'] = $this->get_title();
$data['tip'] = $this->get_title_tip();
$data['colname'] = $this->get_column_name();
$data['columnid'] = $this->get_column_id();
$data['name'] = get_string('selectall');
$data['class'] = $name;
$data['width'] = $width;
echo $renderer->render_column_header($data);
}
protected function display_content($question, $rowclasses): void {
global $OUTPUT;
$checkbox = new checkbox_toggleall('qbank', false, [
'id' => "checkq{$question->id}",
'name' => "q{$question->id}",
'value' => '1',
'label' => get_string('select'),
'labelclasses' => 'accesshide',
]);
echo $OUTPUT->render($checkbox);
}
public function get_required_fields(): array {
return ['q.id'];
}
public function get_default_width(): int {
return 30;
}
}
@@ -0,0 +1,46 @@
<?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/>.
namespace core_question\local\bank;
/**
* Base class to implement actions that can be performed on any column.
*
* A plugin should define subclasses of this for each action it provides, and return an instance of each from
* plugin_feature::get_column_actions(). The action returned from {@see get_action_menu_link()} will be displayed in each column
* header.
*
* @package core_question
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class column_action_base extends view_component {
/**
* A chance for subclasses to initialise themselves, for example to load lang strings,
* without having to override the constructor.
*/
protected function init(): void {
}
/**
* Return the action menu link for this action on the supplied column.
*
* @param column_base $column The column we are providing the action for.
* @return ?\action_menu_link The action to display in the column header.
*/
abstract public function get_action_menu_link(column_base $column): ?\action_menu_link;
}
+477
View File
@@ -0,0 +1,477 @@
<?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 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;
/** @var bool determine whether the column is visible */
public $isvisible = true;
/**
* 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()}
* @param bool $ingoremissing Whether to ignore if the class does not exist.
* @return column_base|null An instance of this class.
*/
public static function from_column_name(view $view, string $columnname, bool $ingoremissing = false): ?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.
* @param string $width A CSS width property value.
*/
public function display_header(array $columnactions = [], string $width = ''): void {
global $PAGE;
$renderer = $PAGE->get_renderer('core_question', 'bank');
$data = [];
$data['sortable'] = true;
$data['extraclasses'] = $this->get_classes();
$sortable = $this->is_sortable();
$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();
$data['columnid'] = $this->get_column_id();
$data['name'] = $title;
$data['class'] = $name;
$data['width'] = $width;
if (!empty($columnactions)) {
$actions = array_map(fn($columnaction) => $columnaction->get_action_menu_link($this), $columnactions);
$actionmenu = new \action_menu($actions);
$data['actionmenu'] = $actionmenu->export_for_template($renderer);
}
echo $renderer->render_column_header($data);
}
/**
* Title for this column. Not used if is_sortable returns an array.
*/
abstract public function get_title();
/**
* 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 $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($sortname, $title, $tip, $defaultreverse = false): string {
global $PAGE;
$sortdata = [];
$currentsort = $this->qbank->get_primary_sort_order($sortname);
$newsortreverse = $defaultreverse;
if ($currentsort) {
$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 == SORT_DESC);
}
$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(),
'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.
*
* This is constructed using the class name and get_column_name(), which must be unique.
*
* The combination of these attributes allows the object to be reconstructed, by splitting the ID into its constituent
* parts then calling {@see from_column_name()}, like this:
* [$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.
*
* @return string The column ID.
*/
final public function get_column_id(): string {
return implode(self::ID_SEPARATOR, [static::class, $this->get_column_name()]);
}
/**
* Any extra class names you would like applied to every cell in this column.
*
* @return array
*/
public function get_extra_classes(): array {
return [];
}
/**
* Return the default column width in pixels.
*
* @return int
*/
public function get_default_width(): int {
return 120;
}
/**
* Output the contents of 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.
*/
abstract protected function display_content($question, $rowclasses);
/**
* Output the closing column tag
*
* @param object $question
* @param string $rowclasses
*/
protected function display_end($question, $rowclasses): void {
$tag = 'td';
if ($this->isheading) {
$tag = 'th';
}
echo \html_writer::end_tag($tag);
}
public function get_extra_joins(): array {
return [];
}
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);
}
}
@@ -0,0 +1,73 @@
<?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/>.
namespace core_question\local\bank;
/**
* Default column manager class
*
* This class defines stub methods that can be overridden by a plugin defining its own column manager.
*
* @package core_question
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_manager_base {
/**
* Sort the list of columns
*
* Sort the provided list of columns into the order implemented in this column manager.
*
* @param array $unsortedcolumns Unordered array of columns
* @return array Columns in the desired order.
*/
public function get_sorted_columns(array $unsortedcolumns): array {
return $unsortedcolumns;
}
/**
* Given an array of columns, set the isvisible attribute.
*
* This base class leave all columns visible.
*
* @param column_base[] $columns
* @return array
*/
public function set_columns_visibility(array $columns): array {
return $columns;
}
/**
* Return a list of actions to display in an action menu for each column.
*
* @param view $qbank Question bank view.
* @return column_action_base[] A list of column actions.
*/
public function get_column_actions(view $qbank): array {
return [];
}
/**
* Given a column, return a value for its width CSS property.
*
* @param column_base $column
* @return string CSS width property value.
*/
public function get_column_width(column_base $column): string {
return $column->get_default_width() . 'px';
}
}
+222
View File
@@ -0,0 +1,222 @@
<?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/>.
namespace core_question\local\bank;
use core\output\datafilter;
/**
* An abstract class for filtering/searching questions.
*
* @package core_question
* @copyright 2013 Ray Morris
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class condition {
/** @var int The default filter type */
const JOINTYPE_DEFAULT = datafilter::JOINTYPE_ANY;
/** @var ?array Filter properties for this condition. */
public ?array $filter;
/** @var string SQL fragment to add to the where clause. */
protected string $where = '';
/** @var array query param used in where. */
protected array $params = [];
/**
* Return title of the condition
*
* @return string title of the condition
*/
abstract public function get_title();
/**
* Return filter class associated with this condition
*
* @return string filter class
*/
abstract public function get_filter_class();
/**
* Extract the required filter from the provided question bank view.
*
* This will look for the filter matching {@see get_condition_key()}
*
* @param view|null $qbank
*/
public function __construct(view $qbank = null) {
if (is_null($qbank)) {
return;
}
$this->filter = static::get_filter_from_list($qbank->get_pagevars('filter'));
// Build where and params.
[$this->where, $this->params] = $this->filter ? static::build_query_from_filter($this->filter) : ['', []];
}
/**
* Whether customisation is allowed.
*
* @return bool
*/
public function allow_custom() {
return true;
}
/**
* Whether multiple values are allowed .
*
* @return bool
*/
public function allow_multiple() {
return true;
}
/**
* Initial values of the condition
*
* @return array
*/
public function get_initial_values() {
return [];
}
/**
* Extra data specific to this condition.
*
* @return \stdClass
*/
public function get_filteroptions(): \stdClass {
return (object)[];
}
/**
* Whether empty value is allowed
*
* @return bool
*/
public function allow_empty() {
return true;
}
/**
* Whether this filter is required - if so it cannot be removed from the list of filters.
*
* @return bool
*/
public function is_required(): bool {
return false;
}
/**
* Return this condition class
*
* @return string
*/
public function get_condition_class() {
return get_class($this);
}
/**
* Each condition will need a unique key to be identified and sequenced by the api.
* Use a unique string for the condition identifier, use string directly, dont need to use language pack.
* Using language pack might break the filter object for multilingual support.
*
* @return string
*/
public static function get_condition_key() {
return '';
}
/**
* Return an SQL fragment to be ANDed into the WHERE clause to filter which questions are shown.
*
* @return string SQL fragment. Must use named parameters.
*/
public function where() {
return $this->where;
}
/**
* Return parameters to be bound to the above WHERE clause fragment.
* @return array parameter name => value.
*/
public function params() {
return $this->params;
}
/**
* Display GUI for selecting criteria for this condition. Displayed when Show More is open.
*
* Compare display_options(), which displays always, whether Show More is open or not.
* @return bool|string HTML form fragment
* @deprecated since Moodle 4.0 MDL-72321 - please do not use this function any more.
* @todo Final deprecation on Moodle 4.1 MDL-72572
*/
public function display_options_adv() {
debugging('Function display_options_adv() is deprecated, please use filtering objects', DEBUG_DEVELOPER);
return false;
}
/**
* Display GUI for selecting criteria for this condition. Displayed always, whether Show More is open or not.
*
* Compare display_options_adv(), which displays when Show More is open.
* @return bool|string HTML form fragment
* @deprecated since Moodle 4.0 MDL-72321 - please do not use this function any more.
* @todo Final deprecation on Moodle 4.1 MDL-72572
*/
public function display_options() {
debugging('Function display_options() is deprecated, please use filtering objects', DEBUG_DEVELOPER);
return false;
}
/**
* Get the list of available joins for the filter.
*
* @return array
*/
public function get_join_list(): array {
return [
datafilter::JOINTYPE_NONE,
datafilter::JOINTYPE_ANY,
datafilter::JOINTYPE_ALL,
];
}
/**
* Given an array of filters, pick the entry that matches the condition key and return it.
*
* @param array $filters Array of filters, keyed by condition.
* @return ?array The filter that matches this condition
*/
public static function get_filter_from_list(array $filters): ?array {
return $filters[static::get_condition_key()] ?? null;
}
/**
* Build query from filter value
*
* @param array $filter filter properties
* @return array where sql and params
*/
public static function build_query_from_filter(array $filter): array {
return ['', []];
}
}
@@ -0,0 +1,98 @@
<?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/>.
namespace core_question\local\bank;
/**
* Converts contextlevels to strings and back to help with reading/writing contexts to/from import/export files.
*
* @package core_question
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class context_to_string_translator {
/**
* @var array used to translate between contextids and strings for this context.
*/
protected $contexttostringarray = [];
/**
* context_to_string_translator constructor.
*
* @param \context[] $contexts
*/
public function __construct($contexts) {
$this->generate_context_to_string_array($contexts);
}
/**
* Context to string.
*
* @param int $contextid
* @return mixed
*/
public function context_to_string($contextid) {
return $this->contexttostringarray[$contextid];
}
/**
* String to context.
*
* @param string $contextname
* @return false|int|string
*/
public function string_to_context($contextname) {
return array_search($contextname, $this->contexttostringarray);
}
/**
* Generate context to array.
*
* @param \context[] $contexts
*/
protected function generate_context_to_string_array($contexts) {
if (!$this->contexttostringarray) {
$catno = 1;
/** @var \context $context */
foreach ($contexts as $context) {
switch ($context->contextlevel) {
case CONTEXT_MODULE :
$contextstring = 'module';
break;
case CONTEXT_COURSE :
$contextstring = 'course';
break;
case CONTEXT_COURSECAT :
$contextstring = "cat$catno";
$catno++;
break;
case CONTEXT_SYSTEM :
$contextstring = 'system';
break;
default:
throw new \coding_exception('Unexpected context level ' .
\context_helper::get_level_name($context->contextlevel) . ' for context ' .
$context->id . ' in generate_context_to_string_array. ' .
'Questions can never exist in this type of context.');
}
$this->contexttostringarray[$context->id] = $contextstring;
}
}
}
}
@@ -0,0 +1,90 @@
<?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/>.
/**
* A question bank column which gathers together all the actions into a menu.
*
* @package core_question
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
use \core\plugininfo\qbank;
/**
* A question bank column which gathers together all the actions into a menu.
*
* This question bank column, if added to the question bank, will
* replace all of the other columns which implement the
* {@see menu_action_column_base} interface and replace them with a single
* column containing an Edit menu.
*
* @copyright 2019 The Open University
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class edit_menu_column extends column_base {
public function get_title() {
return get_string('actions');
}
public function get_name(): string {
return 'editmenu';
}
protected function display_content($question, $rowclasses): void {
global $OUTPUT;
$actions = $this->qbank->get_question_actions();
$menu = new \action_menu();
$menu->set_menu_trigger(get_string('edit'));
$menu->set_boundary('window');
foreach ($actions as $action) {
$action = $action->get_action_menu_link($question);
if ($action) {
$menu->add($action);
}
}
$qtypeactions = \question_bank::get_qtype($question->qtype, false)
->get_extra_question_bank_actions($question);
foreach ($qtypeactions as $action) {
$menu->add($action);
}
echo $OUTPUT->render($menu);
}
public function get_required_fields(): array {
return ['q.qtype'];
}
/**
* Get menuable actions.
*
* @return menu_action_column_base Menuable actions.
*/
public function get_actions(): array {
return $this->actions;
}
public function get_extra_classes(): array {
return ['pr-3'];
}
}
@@ -0,0 +1,120 @@
<?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/>.
/**
* Functions for managing and manipulating question filter conditions
*
* @package core_question
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* Static methods for parsing and formatting data related to filter conditions.
*/
class filter_condition_manager {
/**
* Extract parameters from args list.
*
* @param array $args
* @return array the param and extra param
*/
public static function extract_parameters_from_fragment_args(array $args): array {
$params = [];
if (array_key_exists('filter', $args)) {
$params['filter'] = json_decode($args['filter'], true);
}
if (array_key_exists('cmid', $args)) {
$params['cmid'] = $args['cmid'];
}
if (array_key_exists('courseid', $args)) {
$params['courseid'] = $args['courseid'];
}
$params['jointype'] = $args['jointype'] ?? condition::JOINTYPE_DEFAULT;
$params['qpage'] = $args['qpage'] ?? 0;
$params['qperpage'] = $args['qperpage'] ?? 100;
$params['sortdata'] = json_decode($args['sortdata'] ?? '', true);
$extraparams = json_decode($args['extraparams'] ?? '', true);
return [$params, $extraparams];
}
/**
* List of condition classes
*
* @return condition[] condition classes: [condition_key] = class
*/
public static function get_condition_classes(): array {
$classes = [];
$plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
foreach ($plugins as $componentname => $plugin) {
if (\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
$pluginentrypointobject = new $plugin();
$conditions = $pluginentrypointobject->get_question_filters();
foreach ($conditions as $condition) {
$classes[$condition->get_condition_key()] = $condition->get_condition_class();
}
}
}
return $classes;
}
/**
* Given a JSON-encoded "filter" URL param, create or replace the category filter with the provided category.
*
* @param string $filterparam The json-encoded filter param from the URL, containing the list of filters.
* @param int $newcategoryid The new ID to set for the "category" filter condition's value.
* @return string JSON-encoded filter param with the new category.
*/
public static function update_filter_param_to_category(string $filterparam, int $newcategoryid): string {
$returnfilters = json_decode($filterparam, true);
if (!$returnfilters) {
$returnfilters = [
'category' => [
'name' => 'category',
'jointype' => \core_question\local\bank\condition::JOINTYPE_DEFAULT,
]
];
}
$returnfilters['category']['values'] = [$newcategoryid];
return json_encode($returnfilters);
}
/**
* Unpack filteroptions passed in a request's filter param if required.
*
* Filteroptions are passed via AJAX as an array of {name:, value:} pairs for compatibility with external functions.
*
* @param array $filters List of filters, each optionally including an array of filteroptions.
* @return array The input array, with filteroptions unpacked from [{name:, value:}, ...] to [name => value, ...].
*/
public static function unpack_filteroptions_param(array $filters): array {
foreach ($filters as $name => $filter) {
if (!empty($filter['filteroptions']) && isset(reset($filter['filteroptions'])['name'])) {
$unpacked = [];
foreach ($filter['filteroptions'] as $filteroption) {
$unpacked[$filteroption['name']] = $filteroption['value'];
}
$filters[$name]['filteroptions'] = $unpacked;
}
}
return $filters;
}
}
+54
View File
@@ -0,0 +1,54 @@
<?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/>.
/**
* Helper class for question bank and its plugins.
*
* All the functions which has a potential to be used by different features or
* plugins, should go here.
*
* @package core_question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* Class helper
*
* @package core_question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* Check the status of a plugin and throw exception if not enabled and called manually.
*
* Any action plugin having a php script, should call this function for a safer enable/disable implementation.
*
* @param string $pluginname
* @return void
*/
public static function require_plugin_enabled(string $pluginname): void {
if (!\core\plugininfo\qbank::is_plugin_enabled($pluginname)) {
throw new \moodle_exception('The following plugin is either disabled or missing from disk: ' . $pluginname);
}
}
}
@@ -0,0 +1,73 @@
<?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 to make it easier to implement actions that are menuable_actions.
*
* @package core_question
* @copyright 2019 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* Base class to make it easier to implement actions that are menuable_actions.
*
* Use this class if your action is simple (defined by just a URL, label and icon).
* If your action is not simple enough to fit into the pattern that this
* class implements, then you will have to implement the menuable_action
* interface yourself.
*
* @copyright 2019 Tim Hunt
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated Since Moodle 4.3 MDL-75125 - Use question_action_base instead.
* @todo MDL-78090 This class will be deleted in Moodle 4.7
*/
abstract class menu_action_column_base extends action_column_base implements menuable_action {
/**
* Get the information required to display this action either as a menu item or a separate action column.
*
* If this action cannot apply to this question (e.g. because the user does not have
* permission, then return [null, null, null].
*
* @param \stdClass $question the row from the $question table, augmented with extra information.
* @return array with three elements.
* $url - the URL to perform the action.
* $icon - the icon for this action. E.g. 't/delete'.
* $label - text label to display in the UI (either in the menu, or as a tool-tip on the icon)
*/
abstract protected function get_url_icon_and_label(\stdClass $question);
protected function display_content($question, $rowclasses): void {
debugging('The menu_action_column_base class is deprecated. Please use question_action_base instead.', DEBUG_DEVELOPER);
[$url, $icon, $label] = $this->get_url_icon_and_label($question);
if ($url) {
$this->print_icon($icon, $label, $url);
}
}
public function get_action_menu_link(\stdClass $question): ?\action_menu_link {
debugging('The menu_action_column_base class is deprecated. Please use question_action_base instead.', DEBUG_DEVELOPER);
[$url, $icon, $label] = $this->get_url_icon_and_label($question);
if (!$url) {
return null;
}
return new \action_menu_link_secondary($url, new \pix_icon($icon, ''), $label);
}
}
@@ -0,0 +1,57 @@
<?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/>.
/**
* Interface to indicate that a question bank column can go in the action menu.
*
* @package core_question
* @copyright 2019 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* Interface to indicate that a question bank column can go in the action menu.
*
* If a question bank column implements this interface, and if the {@see edit_menu_column}
* is present in the question bank view, then the 'column' will be shown as an entry in the
* edit menu instead of as a separate column.
*
* Probably most columns that want to implement this will be subclasses of
* {@see action_column_base}, and most such columns should probably implement
* this interface.
*
* If your column is a simple action, you can probably save duplicated code by
* using the base class action_column_menuable as an easy way to implement both
* action_column_base and this interface.
*
* @copyright 2019 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated Since Moodle 4.3 MDL-75125 - Use question_action_base instead.
* @todo MDL-78090 This interface will be deleted in Moodle 4.7
*/
interface menuable_action {
/**
* Return the appropriate action menu link, or null if it does not apply to this question.
*
* @param \stdClass $question data about the question being displayed in this row.
* @return \action_menu_link|null the action, if applicable to this question.
* @deprecated Since Moodle 4.3
*/
public function get_action_menu_link(\stdClass $question): ?\action_menu_link;
}
@@ -0,0 +1,64 @@
<?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 class for navigation node.
*
* @package core_question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* Class navigation_node_base is the base class for navigation node.
*
* @package core_question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class navigation_node_base {
/**
* Title for this node.
*/
abstract public function get_navigation_title();
/**
* Key for this node.
*/
abstract public function get_navigation_key();
/**
* URL for this node
*/
abstract public function get_navigation_url();
/**
* Tab capabilities.
*
* If it has capabilities to be checked, it will return the array of capabilities.
*
* @return null|array
*/
public function get_navigation_capabilities(): ?array {
return null;
}
}
@@ -0,0 +1,117 @@
<?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 class for qbank plugins.
*
* Every qbank plugin must extent this class.
*
* @package core_question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
use core\context;
use qbank_columnsortorder\local\qbank\column_action_move;
use qbank_columnsortorder\local\qbank\column_action_remove;
use qbank_columnsortorder\local\qbank\column_action_resize;
/**
* Class plugin_features_base is the base class for qbank plugins.
*
* @package core_question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_features_base {
/**
* This method will return the array of objects to be rendered as a part of question bank columns.
*
* @param view $qbank
* @return array
*/
public function get_question_columns(view $qbank): ?array {
return [];
}
/**
* This method will return the array of objects to be rendered as a part of question bank actions.
*
* @param view $qbank
* @return question_action_base[]
*/
public function get_question_actions(view $qbank): array {
return [];
}
/**
* This method will return the object for the navigation node.
*
* @return null|navigation_node_base
*/
public function get_navigation_node(): ?navigation_node_base {
return null;
}
/**
* This method will return the array objects for the bulk actions ui.
*
* @return bulk_action_base[]
*/
public function get_bulk_actions() {
return [];
}
/**
* This method will return a column manager object, if this plugin provides one.
*
* @return ?column_manager_base
*/
public function get_column_manager(): ?column_manager_base {
return null;
}
/**
* This method will return an array of renderable objects, for adding additional controls to the question bank screen.
*
* The array returned can include a numeric index for each object, to indicate the position in which it should be displayed
* relative to other controls. If two plugins return controls with the same position, they will be displayed after one another,
* based on the alphabetical order of the plugin component names.
*
* @param view $qbank The question bank view.
* @param context $context The current context, for permission checks.
* @param int $categoryid The current question category ID.
* @return \renderable[]
*/
public function get_question_bank_controls(view $qbank, context $context, int $categoryid): array {
return [];
}
/**
* Return search conditions for the plugin.
*
* @param view|null $qbank
* @return condition[]
*/
public function get_question_filters(view $qbank = null): array {
return [];
}
}
@@ -0,0 +1,77 @@
<?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 to make it easier to implement actions that are menuable_actions.
*
* @package core_question
* @copyright 2019 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* Base class to make it easier to implement actions that are menuable_actions.
*
* Use this class if your action is simple (defined by just a URL, label and icon).
* If your action is not simple enough to fit into the pattern that this
* class implements, then you will have to implement the menuable_action
* interface yourself.
*
* @copyright 2019 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 question_action_base extends view_component {
/**
* Get the information required to display this action either as a menu item or a separate action column.
*
* For most actions, it should be sufficient to override just this method. {@see get_action_menu_link()} is the public interface
* and handles building a renderable menu link object from this data.
*
* If this action cannot apply to this question (e.g. because the user does not have
* permission, then return [null, null, null].
*
* @param \stdClass $question the row from the $question table, augmented with extra information.
* @return array with three elements.
* $url - the URL to perform the action.
* $icon - the icon for this action. E.g. 't/delete'.
* $label - text label to display in the UI (either in the menu, or as a tool-tip on the icon)
*/
protected function get_url_icon_and_label(\stdClass $question): array {
return [null, null, null];
}
/**
* Return the action menu link for this action on the supplied question.
*
* For most actions, you will just need to override {@see get_url_icon_and_label()}. You only need to override
* this method if you need to pass additional attributes to {@see action_menu_link_secondary}, or use a different class to
* render the link.
*
* @param \stdClass $question
* @return \action_menu_link|null
*/
public function get_action_menu_link(\stdClass $question): ?\action_menu_link {
[$url, $icon, $label] = $this->get_url_icon_and_label($question);
if (!$url) {
return null;
}
return new \action_menu_link_secondary($url, new \pix_icon($icon, ''), $label);
}
}
@@ -0,0 +1,222 @@
<?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/>.
namespace core_question\local\bank;
/**
* Tracks all the contexts related to the one we are currently editing questions and provides helper methods to check permissions.
*
* @package core_question
* @copyright 2007 Jamie Pratt me@jamiep.org
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_edit_contexts {
/**
* @var \string[][] array of the capabilities.
*/
public static $caps = [
'editq' => [
'moodle/question:add',
'moodle/question:editmine',
'moodle/question:editall',
'moodle/question:viewmine',
'moodle/question:viewall',
'moodle/question:usemine',
'moodle/question:useall',
'moodle/question:movemine',
'moodle/question:moveall'],
'questions' => [
'moodle/question:add',
'moodle/question:editmine',
'moodle/question:editall',
'moodle/question:viewmine',
'moodle/question:viewall',
'moodle/question:movemine',
'moodle/question:moveall'],
'categories' => [
'moodle/question:managecategory'],
'import' => [
'moodle/question:add'],
'export' => [
'moodle/question:viewall',
'moodle/question:viewmine']];
/**
* @var array of contexts.
*/
protected $allcontexts;
/**
* Constructor
* @param \context $thiscontext the current context.
*/
public function __construct(\context $thiscontext) {
$this->allcontexts = array_values($thiscontext->get_parent_contexts(true));
}
/**
* Get all the contexts.
*
* @return \context[] all parent contexts
*/
public function all() {
return $this->allcontexts;
}
/**
* Get the lowest context.
*
* @return \context lowest context which must be either the module or course context
*/
public function lowest() {
return $this->allcontexts[0];
}
/**
* Get the contexts having cap.
*
* @param string $cap capability
* @return \context[] parent contexts having capability, zero based index
*/
public function having_cap($cap) {
$contextswithcap = [];
foreach ($this->allcontexts as $context) {
if (has_capability($cap, $context)) {
$contextswithcap[] = $context;
}
}
return $contextswithcap;
}
/**
* Get the contexts having at least one cap.
*
* @param array $caps capabilities
* @return \context[] parent contexts having at least one of $caps, zero based index
*/
public function having_one_cap($caps) {
$contextswithacap = [];
foreach ($this->allcontexts as $context) {
foreach ($caps as $cap) {
if (has_capability($cap, $context)) {
$contextswithacap[] = $context;
break; // Done with caps loop.
}
}
}
return $contextswithacap;
}
/**
* Context having at least one cap.
*
* @param string $tabname edit tab name
* @return \context[] parent contexts having at least one of $caps, zero based index
*/
public function having_one_edit_tab_cap($tabname) {
return $this->having_one_cap(self::$caps[$tabname]);
}
/**
* Contexts for adding question and also using it.
*
* @return \context[] those contexts where a user can add a question and then use it.
*/
public function having_add_and_use() {
$contextswithcap = [];
foreach ($this->allcontexts as $context) {
if (!has_capability('moodle/question:add', $context)) {
continue;
}
if (!has_any_capability(['moodle/question:useall', 'moodle/question:usemine'], $context)) {
continue;
}
$contextswithcap[] = $context;
}
return $contextswithcap;
}
/**
* Has at least one parent context got the cap $cap?
*
* @param string $cap capability
* @return boolean
*/
public function have_cap($cap) {
return (count($this->having_cap($cap)));
}
/**
* Has at least one parent context got one of the caps $caps?
*
* @param array $caps capability
* @return boolean
*/
public function have_one_cap($caps) {
foreach ($caps as $cap) {
if ($this->have_cap($cap)) {
return true;
}
}
return false;
}
/**
* Has at least one parent context got one of the caps for actions on $tabname
*
* @param string $tabname edit tab name
* @return boolean
*/
public function have_one_edit_tab_cap($tabname) {
return $this->have_one_cap(self::$caps[$tabname]);
}
/**
* Throw error if at least one parent context hasn't got the cap $cap
*
* @param string $cap capability
*/
public function require_cap($cap) {
if (!$this->have_cap($cap)) {
throw new \moodle_exception('nopermissions', '', '', $cap);
}
}
/**
* Throw error if at least one parent context hasn't got one of the caps $caps
*
* @param array $caps capabilities
*/
public function require_one_cap($caps) {
if (!$this->have_one_cap($caps)) {
$capsstring = join(', ', $caps);
throw new \moodle_exception('nopermissions', '', '', $capsstring);
}
}
/**
* Throw error if at least one parent context hasn't got one of the caps $caps
*
* @param string $tabname edit tab name
*/
public function require_one_edit_tab_cap($tabname) {
if (!$this->have_one_edit_tab_cap($tabname)) {
throw new \moodle_exception('nopermissions', '', '', 'access question edit tab '.$tabname);
}
}
}
@@ -0,0 +1,43 @@
<?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/>.
namespace core_question\local\bank;
/**
* Class question_version_status contains the statuses for a question.
*
* @package core_question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_version_status {
/**
* Const if the question is ready to use.
*/
const QUESTION_STATUS_READY = 'ready';
/**
* Const if the question is hidden.
*/
const QUESTION_STATUS_HIDDEN = 'hidden';
/**
* const if the question is in draft.
*/
const QUESTION_STATUS_DRAFT = 'draft';
}
@@ -0,0 +1,609 @@
<?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/>.
/**
* A class for efficiently finds questions at random from the question bank.
*
* @package core_question
* @copyright 2015 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* This class efficiently finds questions at random from the question bank.
*
* You can ask for questions at random one at a time. Each time you ask, you
* pass a category id, and whether to pick from that category and all subcategories
* or just that category.
*
* The number of teams each question has been used is tracked, and we will always
* return a question from among those elegible that has been used the fewest times.
* So, if there are questions that have not been used yet in the category asked for,
* one of those will be returned. However, within one instantiation of this class,
* we will never return a given question more than once, and we will never return
* questions passed into the constructor as $usedquestions.
*
* @copyright 2015 The Open University
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class random_question_loader {
/** @var \qubaid_condition which usages to consider previous attempts from. */
protected $qubaids;
/** @var array qtypes that cannot be used by random questions. */
protected $excludedqtypes;
/** @var array categoryid & include subcategories => num previous uses => questionid => 1. */
protected $availablequestionscache = [];
/**
* @var array questionid => num recent uses. Questions that have been used,
* but that is not yet recorded in the DB.
*/
protected $recentlyusedquestions;
/**
* Constructor.
*
* @param \qubaid_condition $qubaids the usages to consider when counting previous uses of each question.
* @param array $usedquestions questionid => number of times used count. If we should allow for
* further existing uses of a question in addition to the ones in $qubaids.
*/
public function __construct(\qubaid_condition $qubaids, array $usedquestions = []) {
$this->qubaids = $qubaids;
$this->recentlyusedquestions = $usedquestions;
foreach (\question_bank::get_all_qtypes() as $qtype) {
if (!$qtype->is_usable_by_random()) {
$this->excludedqtypes[] = $qtype->name();
}
}
}
/**
* Pick a random question based on filter conditions
*
* @param array $filters filter array
* @return int|null
*/
public function get_next_filtered_question_id(array $filters): ?int {
$this->ensure_filtered_questions_loaded($filters);
$key = $this->get_filtered_questions_key($filters);
if (empty($this->availablequestionscache[$key])) {
return null;
}
reset($this->availablequestionscache[$key]);
$lowestcount = key($this->availablequestionscache[$key]);
reset($this->availablequestionscache[$key][$lowestcount]);
$questionid = key($this->availablequestionscache[$key][$lowestcount]);
$this->use_question($questionid);
return $questionid;
}
/**
* Pick a question at random from the given category, from among those with the fewest uses.
* If an array of tag ids are specified, then only the questions that are tagged with ALL those tags will be selected.
*
* It is up the the caller to verify that the cateogry exists. An unknown category
* behaves like an empty one.
*
* @param int $categoryid the id of a category in the question bank.
* @param bool $includesubcategories wether to pick a question from exactly
* that category, or that category and subcategories.
* @param array $tagids An array of tag ids. A question has to be tagged with all the provided tagids (if any)
* in order to be eligible for being picked.
* @return int|null the id of the question picked, or null if there aren't any.
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function get_next_question_id($categoryid, $includesubcategories, $tagids = []): ?int {
debugging(
'Function get_next_question_id() is deprecated, please use get_next_filtered_question_id() instead.',
DEBUG_DEVELOPER
);
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids);
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
if (empty($this->availablequestionscache[$categorykey])) {
return null;
}
reset($this->availablequestionscache[$categorykey]);
$lowestcount = key($this->availablequestionscache[$categorykey]);
reset($this->availablequestionscache[$categorykey][$lowestcount]);
$questionid = key($this->availablequestionscache[$categorykey][$lowestcount]);
$this->use_question($questionid);
return $questionid;
}
/**
* Key for filtered questions.
* This function replace get_category_key
*
* @param array $filters filter array
* @return String
*/
protected function get_filtered_questions_key(array $filters): String {
return sha1(json_encode($filters));
}
/**
* Get the key into {@see $availablequestionscache} for this combination of options.
*
* @param int $categoryid the id of a category in the question bank.
* @param bool $includesubcategories wether to pick a question from exactly
* that category, or that category and subcategories.
* @param array $tagids an array of tag ids.
* @return string the cache key.
*
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
protected function get_category_key($categoryid, $includesubcategories, $tagids = []): string {
debugging(
'Function get_category_key() is deprecated, please get_fitlered_questions_key instead.',
DEBUG_DEVELOPER
);
if ($includesubcategories) {
$key = $categoryid . '|1';
} else {
$key = $categoryid . '|0';
}
if (!empty($tagids)) {
$key .= '|' . implode('|', $tagids);
}
return $key;
}
/**
* Populate {@see $availablequestionscache} according to filter conditions.
*
* @param array $filters filter array
* @return void
*/
protected function ensure_filtered_questions_loaded(array $filters) {
global $DB;
// Check if this is already done.
$key = $this->get_filtered_questions_key($filters);
if (isset($this->availablequestionscache[$key])) {
// Data is already in the cache, nothing to do.
return;
}
// Build filter conditions.
$params = [];
$filterconditions = [];
foreach (filter_condition_manager::get_condition_classes() as $conditionclass) {
$filter = $conditionclass::get_filter_from_list($filters);
if (is_null($filter)) {
continue;
}
[$filterwhere, $filterparams] = $conditionclass::build_query_from_filter($filter);
if (!empty($filterwhere)) {
$filterconditions[] = '(' . $filterwhere . ')';
}
if (!empty($filterparams)) {
$params = array_merge($params, $filterparams);
}
}
$filtercondition = $filterconditions ? 'AND ' . implode(' AND ', $filterconditions) : '';
// Prepare qtype check.
[$qtypecondition, $qtypeparams] = $DB->get_in_or_equal($this->excludedqtypes,
SQL_PARAMS_NAMED, 'excludedqtype', false);
if ($qtypecondition) {
$qtypecondition = 'AND q.qtype ' . $qtypecondition;
}
$questionidsandcounts = $DB->get_records_sql_menu("
SELECT q.id,
(
SELECT COUNT(1)
FROM {$this->qubaids->from_question_attempts('qa')}
WHERE qa.questionid = q.id AND {$this->qubaids->where()}
) AS previous_attempts
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
WHERE q.parent = :noparent
$qtypecondition
$filtercondition
AND qv.version = (
SELECT MAX(version)
FROM {question_versions}
WHERE questionbankentryid = qbe.id
AND status = :ready
)
ORDER BY previous_attempts
", array_merge(
$params,
$this->qubaids->from_where_params(),
['noparent' => 0, 'ready' => question_version_status::QUESTION_STATUS_READY],
$qtypeparams,
));
if (!$questionidsandcounts) {
// No questions in this category.
$this->availablequestionscache[$key] = [];
return;
}
// Put all the questions with each value of $prevusecount in separate arrays.
$idsbyusecount = [];
foreach ($questionidsandcounts as $questionid => $prevusecount) {
if (isset($this->recentlyusedquestions[$questionid])) {
// Recently used questions are never returned.
continue;
}
$idsbyusecount[$prevusecount][] = $questionid;
}
// Now put that data into our cache. For each count, we need to shuffle
// questionids, and make those the keys of an array.
$this->availablequestionscache[$key] = [];
foreach ($idsbyusecount as $prevusecount => $questionids) {
shuffle($questionids);
$this->availablequestionscache[$key][$prevusecount] = array_combine(
$questionids, array_fill(0, count($questionids), 1));
}
ksort($this->availablequestionscache[$key]);
}
/**
* Populate {@see $availablequestionscache} for this combination of options.
*
* @param int $categoryid The id of a category in the question bank.
* @param bool $includesubcategories Whether to pick a question from exactly
* that category, or that category and subcategories.
* @param array $tagids An array of tag ids. If an array is provided, then
* only the questions that are tagged with ALL the provided tagids will be loaded.
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
protected function ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids = []): void {
debugging(
'Function ensure_questions_for_category_loaded() is deprecated, please use the function ' .
'ensure_filtered_questions_loaded.',
DEBUG_DEVELOPER
);
global $DB;
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
if (isset($this->availablequestionscache[$categorykey])) {
// Data is already in the cache, nothing to do.
return;
}
// Load the available questions from the question bank.
if ($includesubcategories) {
$categoryids = question_categorylist($categoryid);
} else {
$categoryids = [$categoryid];
}
list($extraconditions, $extraparams) = $DB->get_in_or_equal($this->excludedqtypes,
SQL_PARAMS_NAMED, 'excludedqtype', false);
$questionidsandcounts = \question_bank::get_finder()->get_questions_from_categories_and_tags_with_usage_counts(
$categoryids, $this->qubaids, 'q.qtype ' . $extraconditions, $extraparams, $tagids);
if (!$questionidsandcounts) {
// No questions in this category.
$this->availablequestionscache[$categorykey] = [];
return;
}
// Put all the questions with each value of $prevusecount in separate arrays.
$idsbyusecount = [];
foreach ($questionidsandcounts as $questionid => $prevusecount) {
if (isset($this->recentlyusedquestions[$questionid])) {
// Recently used questions are never returned.
continue;
}
$idsbyusecount[$prevusecount][] = $questionid;
}
// Now put that data into our cache. For each count, we need to shuffle
// questionids, and make those the keys of an array.
$this->availablequestionscache[$categorykey] = [];
foreach ($idsbyusecount as $prevusecount => $questionids) {
shuffle($questionids);
$this->availablequestionscache[$categorykey][$prevusecount] = array_combine(
$questionids, array_fill(0, count($questionids), 1));
}
ksort($this->availablequestionscache[$categorykey]);
}
/**
* Update the internal data structures to indicate that a given question has
* been used one more time.
*
* @param int $questionid the question that is being used.
*/
protected function use_question($questionid): void {
if (isset($this->recentlyusedquestions[$questionid])) {
$this->recentlyusedquestions[$questionid] += 1;
} else {
$this->recentlyusedquestions[$questionid] = 1;
}
foreach ($this->availablequestionscache as $categorykey => $questionsforcategory) {
foreach ($questionsforcategory as $numuses => $questionids) {
if (!isset($questionids[$questionid])) {
continue;
}
unset($this->availablequestionscache[$categorykey][$numuses][$questionid]);
if (empty($this->availablequestionscache[$categorykey][$numuses])) {
unset($this->availablequestionscache[$categorykey][$numuses]);
}
}
}
}
/**
* Get filtered questions.
*
* @param array $filters filter array
* @return array list of filtered questions
*/
protected function get_filtered_question_ids(array $filters): array {
$this->ensure_filtered_questions_loaded($filters);
$key = $this->get_filtered_questions_key($filters);
$cachedvalues = $this->availablequestionscache[$key];
$questionids = [];
foreach ($cachedvalues as $usecount => $ids) {
$questionids = array_merge($questionids, array_keys($ids));
}
return $questionids;
}
/**
* Get the list of available question ids for the given criteria.
*
* @param int $categoryid The id of a category in the question bank.
* @param bool $includesubcategories Whether to pick a question from exactly
* that category, or that category and subcategories.
* @param array $tagids An array of tag ids. If an array is provided, then
* only the questions that are tagged with ALL the provided tagids will be loaded.
* @return int[] The list of question ids
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
protected function get_question_ids($categoryid, $includesubcategories, $tagids = []): array {
debugging(
'Function get_question_ids() is deprecated, please use get_filtered_question_ids() instead.',
DEBUG_DEVELOPER
);
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids);
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
$cachedvalues = $this->availablequestionscache[$categorykey];
$questionids = [];
foreach ($cachedvalues as $usecount => $ids) {
$questionids = array_merge($questionids, array_keys($ids));
}
return $questionids;
}
/**
* Check whether a given question is available in a given category. If so, mark it used.
* If an optional list of tag ids are provided, then the question must be tagged with
* ALL of the provided tags to be considered as available.
*
* @param array $filters filter array
* @param int $questionid the question that is being used.
* @return bool whether the question is available in the requested category.
*/
public function is_filtered_question_available(array $filters, int $questionid): bool {
$this->ensure_filtered_questions_loaded($filters);
$categorykey = $this->get_filtered_questions_key($filters);
foreach ($this->availablequestionscache[$categorykey] as $questionids) {
if (isset($questionids[$questionid])) {
$this->use_question($questionid);
return true;
}
}
return false;
}
/**
* Check whether a given question is available in a given category. If so, mark it used.
* If an optional list of tag ids are provided, then the question must be tagged with
* ALL of the provided tags to be considered as available.
*
* @param int $categoryid the id of a category in the question bank.
* @param bool $includesubcategories wether to pick a question from exactly
* that category, or that category and subcategories.
* @param int $questionid the question that is being used.
* @param array $tagids An array of tag ids. Only the questions that are tagged with all the provided tagids can be available.
* @return bool whether the question is available in the requested category.
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function is_question_available($categoryid, $includesubcategories, $questionid, $tagids = []): bool {
debugging(
'Function is_question_available() is deprecated, please use is_filtered_question_available() instead.',
DEBUG_DEVELOPER
);
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids);
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
foreach ($this->availablequestionscache[$categorykey] as $questionids) {
if (isset($questionids[$questionid])) {
$this->use_question($questionid);
return true;
}
}
return false;
}
/**
* Get the list of available questions for the given criteria.
*
* @param array $filters filter array
* @param int $limit Maximum number of results to return.
* @param int $offset Number of items to skip from the begging of the result set.
* @param string[] $fields The fields to return for each question.
* @return \stdClass[] The list of question records
*/
public function get_filtered_questions($filters, $limit = 100, $offset = 0, $fields = []) {
global $DB;
$questionids = $this->get_filtered_question_ids($filters);
if (empty($questionids)) {
return [];
}
if (empty($fields)) {
// Return all fields.
$fieldsstring = '*';
} else {
$fieldsstring = implode(',', $fields);
}
// Create the query to get the questions (validate that at least we have a question id. If not, do not execute the sql).
$hasquestions = false;
if (!empty($questionids)) {
$hasquestions = true;
}
if ($hasquestions) {
[$condition, $param] = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid');
$condition = 'WHERE q.id ' . $condition;
$sql = "SELECT {$fieldsstring}
FROM (SELECT q.*, qbe.questioncategoryid as category
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
{$condition}) q
ORDER BY q.id";
return $DB->get_records_sql($sql, $param, $offset, $limit);
} else {
return [];
}
}
/**
* Get the list of available questions for the given criteria.
*
* @param int $categoryid The id of a category in the question bank.
* @param bool $includesubcategories Whether to pick a question from exactly
* that category, or that category and subcategories.
* @param array $tagids An array of tag ids. If an array is provided, then
* only the questions that are tagged with ALL the provided tagids will be loaded.
* @param int $limit Maximum number of results to return.
* @param int $offset Number of items to skip from the begging of the result set.
* @param string[] $fields The fields to return for each question.
* @return \stdClass[] The list of question records
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function get_questions($categoryid, $includesubcategories, $tagids = [], $limit = 100, $offset = 0, $fields = []) {
debugging(
'Function get_questions() is deprecated, please use get_filtered_questions() instead.',
DEBUG_DEVELOPER
);
global $DB;
$questionids = $this->get_question_ids($categoryid, $includesubcategories, $tagids);
if (empty($questionids)) {
return [];
}
if (empty($fields)) {
// Return all fields.
$fieldsstring = '*';
} else {
$fieldsstring = implode(',', $fields);
}
// Create the query to get the questions (validate that at least we have a question id. If not, do not execute the sql).
$hasquestions = false;
if (!empty($questionids)) {
$hasquestions = true;
}
if ($hasquestions) {
list($condition, $param) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid');
$condition = 'WHERE q.id ' . $condition;
$sql = "SELECT {$fieldsstring}
FROM (SELECT q.*, qbe.questioncategoryid as category
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
{$condition}) q ORDER BY q.id";
return $DB->get_records_sql($sql, $param, $offset, $limit);
} else {
return [];
}
}
/**
* Count number of filtered questions
*
* @param array $filters filter array
* @return int number of question
*/
public function count_filtered_questions(array $filters): int {
$questionids = $this->get_filtered_question_ids($filters);
return count($questionids);
}
/**
* Count the number of available questions for the given criteria.
*
* @param int $categoryid The id of a category in the question bank.
* @param bool $includesubcategories Whether to pick a question from exactly
* that category, or that category and subcategories.
* @param array $tagids An array of tag ids. If an array is provided, then
* only the questions that are tagged with ALL the provided tagids will be loaded.
* @return int The number of questions matching the criteria.
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function count_questions($categoryid, $includesubcategories, $tagids = []): int {
debugging(
'Function count_questions() is deprecated, please use count_filtered_questions() instead.',
DEBUG_DEVELOPER
);
$questionids = $this->get_question_ids($categoryid, $includesubcategories, $tagids);
return count($questionids);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?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 'columns' that are actually displayed as a row following the main question row.
*
* @package core_question
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\local\bank;
/**
* Base class for 'columns' that are actually displayed as a row following the main question row.
*
* @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 row_base extends column_base {
/**
* Check if the column is an extra row of not.
*/
public function is_extra_row(): bool {
return true;
}
/**
* 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 {
if ($rowclasses) {
echo \html_writer::start_tag('tr', ['class' => $rowclasses]);
} else {
echo \html_writer::start_tag('tr');
}
echo \html_writer::start_tag('td',
['colspan' => $this->qbank->get_column_count(), 'class' => $this->get_name()]);
}
/**
* Output the closing column tag
*
* @param object $question
* @param string $rowclasses
*/
protected function display_end($question, $rowclasses): void {
echo \html_writer::end_tag('td');
echo \html_writer::end_tag('tr');
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,102 @@
<?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/>.
namespace core_question\local\bank;
/**
* Abstract class to define functionality shared by all pluggable components used in the question bank view.
*
* @package core_question
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class view_component {
/** @var int value we return from get_menu_position here. Subclasses should override this. */
const MENU_POSITION_NOT_SET = 6666;
/** @var view Question bank view. */
protected $qbank;
/**
* Constructor.
* @param view $qbank the question bank view we are helping to render.
*/
public function __construct(view $qbank) {
$this->qbank = $qbank;
$this->init();
}
/**
* A chance for subclasses to initialise themselves, for example to load lang strings,
* without having to override the constructor.
*/
protected function init(): void {
}
/**
* Return an integer to indicate the desired position in the menu for this link, smaller at the top.
*
* The standard menu items in Moodle core return these numbers:
* 100 preview_action
* 200 edit_action
* 250 copy_action
* 300 tags_action
* 400 delete_action
* 500 history_action
* 600 export_xml_action
* (So, if you want your action at a particular place in the order, there should be space.)
*
* If two actions get the same order number, then the tie-break on the sort
* is plugin name, then the order returned by get_question_actions for that plugin.
*
* @return int desired position. Smallest at the top.
*/
public function get_menu_position(): int {
// We return a big number by default, which is after all the standard core links,
// so they go first. This should be overridden by all plugins, and not overriding will
// generate a debugging warning from {@see \core_question\local\bank\view::init_question_actions()}.
return self::MENU_POSITION_NOT_SET;
}
/**
* Return an array 'table_alias' => 'JOIN clause' to bring in any data that
* this feature requires.
*
* The return values for all the features will be checked. It is OK if two
* features join in the same table with the same alias and identical JOIN clauses.
* If two features try to use the same alias with different joins, you get an error.
* Tables included by default are question (alias q) and those defined in {@see view::get_required_joins()}
*
* 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 string[] '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 string[] fields required.
*/
public function get_required_fields(): array {
return [];
}
}
@@ -0,0 +1,180 @@
<?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/>.
namespace core_question\local\statistics;
use core_question\local\bank\column_base;
use core_question\statistics\questions\all_calculated_for_qubaid_condition;
use core_component;
/**
* Helper to efficiently load all the statistics for a set of questions.
*
* If you are implementing a question bank column, do not use this method directly.
* Instead, override the {@see column_base::get_required_statistics_fields()} method
* in your column class, and the question bank view will take care of it for you.
*
* @package core_question
* @copyright 2023 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class statistics_bulk_loader {
/**
* Load and aggregate the requested statistics for all the places where the given questions are used.
*
* The returned array will contain a values for each questionid and field, which will be null if the value is not available.
*
* @param int[] $questionids array of question ids.
* @param string[] $requiredstatistics array of the fields required, e.g. ['facility', 'discriminationindex'].
* @return float[][] if a value is not available, it will be set to null.
*/
public static function load_aggregate_statistics(array $questionids, array $requiredstatistics): array {
// Prevent unnecessary statistics calculations.
if (empty($requiredstatistics)) {
$aggregates = [];
foreach ($questionids as $questionid) {
$aggregates[$questionid] = [];
}
return $aggregates;
}
$places = self::get_all_places_where_questions_were_attempted($questionids);
// Set up blank two-dimensional arrays to store the running totals. Indexed by questionid and field name.
$zerovaluesforonequestion = array_combine($requiredstatistics, array_fill(0, count($requiredstatistics), 0));
$counts = array_combine($questionids, array_fill(0, count($questionids), $zerovaluesforonequestion));
$sums = array_combine($questionids, array_fill(0, count($questionids), $zerovaluesforonequestion));
// Load the data for each place, and add to the running totals.
foreach ($places as $place) {
$statistics = self::load_statistics_for_place($place->component,
\context::instance_by_id($place->contextid));
if ($statistics === null) {
continue;
}
foreach ($questionids as $questionid) {
foreach ($requiredstatistics as $item) {
$value = self::extract_item_value($statistics, $questionid, $item);
if ($value === null) {
continue;
}
$counts[$questionid][$item] += 1;
$sums[$questionid][$item] += $value;
}
}
}
// Compute the averages from the final totals.
$aggregates = [];
foreach ($questionids as $questionid) {
$aggregates[$questionid] = [];
foreach ($requiredstatistics as $item) {
if ($counts[$questionid][$item] > 0) {
$aggregates[$questionid][$item] = $sums[$questionid][$item] / $counts[$questionid][$item];
} else {
$aggregates[$questionid][$item] = null;
}
}
}
return $aggregates;
}
/**
* For a list of questions find all the places, defined by (component, contextid), where there are attempts.
*
* @param int[] $questionids array of question ids that we are interested in.
* @return \stdClass[] list of objects with fields ->component and ->contextid.
*/
protected static function get_all_places_where_questions_were_attempted(array $questionids): array {
global $DB;
[$questionidcondition, $params] = $DB->get_in_or_equal($questionids);
// The MIN(qu.id) is just to ensure that the rows have a unique key.
$places = $DB->get_records_sql("
SELECT MIN(qu.id) AS somethingunique, qu.component, qu.contextid
FROM {question_usages} qu
JOIN {question_attempts} qatt ON qatt.questionusageid = qu.id
JOIN {context} ctx ON ctx.id = qu.contextid
WHERE qatt.questionid $questionidcondition
GROUP BY qu.component, qu.contextid
ORDER BY qu.contextid ASC
", $params);
// Strip out the unwanted ids.
$places = array_values($places);
foreach ($places as $place) {
unset($place->somethingunique);
}
return $places;
}
/**
* Load the question statistics for all the attempts belonging to a particular component in a particular context.
*
* @param string $component frankenstyle component name, e.g. 'mod_quiz'.
* @param \context $context the context to load the statistics for.
* @return all_calculated_for_qubaid_condition|null question statistics.
*/
protected static function load_statistics_for_place(
string $component,
\context $context
): ?all_calculated_for_qubaid_condition {
// This check is basically if (component_exists).
if (empty(core_component::get_component_directory($component))) {
return null;
}
if (!component_callback_exists($component, 'calculate_question_stats')) {
return null;
}
return component_callback($component, 'calculate_question_stats', [$context]);
}
/**
* Extract the value for one question and one type of statistic from a set of statistics.
*
* @param all_calculated_for_qubaid_condition $statistics the batch of statistics.
* @param int $questionid a question id.
* @param string $item one of the field names in all_calculated_for_qubaid_condition, e.g. 'facility'.
* @return float|null the required value.
*/
protected static function extract_item_value(all_calculated_for_qubaid_condition $statistics,
int $questionid, string $item): ?float {
// Look in main questions.
foreach ($statistics->questionstats as $stats) {
if ($stats->questionid == $questionid && isset($stats->$item)) {
return $stats->$item;
}
}
// If not found, look in sub questions.
foreach ($statistics->subquestionstats as $stats) {
if ($stats->questionid == $questionid && isset($stats->$item)) {
return $stats->$item;
}
}
return null;
}
}