MDL-72553 qbank_customfields: Add custom fields for question types
Custom fields can now be added to question types. Site administrators can configure the custom fields and filed types available. Teachers and question creators can then use these fields for extra question metadata when creating questions. Question creators can control if the fields are displayed to students when they are taking a quiz or not. This features uses the core Moodle custom field API. Co-Authored-By: Matt Porritt <[email protected]> Co-Authored-By: Safat Shahin <[email protected]> Co-Authored-By: Marc-Alexandre Ghaly <[email protected]>
This commit is contained in:
committed by
Safat Shahin
co-authored by
Safat Shahin
Marc-Alexandre Ghaly
parent
51149a78cc
commit
556e2f1aec
@@ -1952,6 +1952,7 @@ class core_plugin_manager {
|
||||
'qbank' => [
|
||||
'bulkmove',
|
||||
'comment',
|
||||
'customfields',
|
||||
'deletequestion',
|
||||
'editquestion',
|
||||
'exporttoxml',
|
||||
|
||||
@@ -46,6 +46,10 @@ class qbank extends base {
|
||||
return new \moodle_url('/admin/settings.php', ['section' => 'manageqbanks']);
|
||||
}
|
||||
|
||||
public function get_settings_section_name() {
|
||||
return $this->type . '_' . $this->name;
|
||||
}
|
||||
|
||||
public static function get_plugins($type, $typerootdir, $typeclass, $pluginman): array {
|
||||
global $CFG;
|
||||
|
||||
|
||||
@@ -377,6 +377,10 @@ function question_delete_question($questionid) {
|
||||
// Delete all tag instances.
|
||||
core_tag_tag::remove_all_item_tags('core_question', 'question', $question->id);
|
||||
|
||||
// Delete the custom filed data for the question.
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$customfieldhandler->delete_instance($question->id);
|
||||
|
||||
// Now recursively delete all child questions
|
||||
if ($children = $DB->get_records('question',
|
||||
array('parent' => $questionid), '', 'id, qtype')) {
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
|
||||
namespace mod_quiz\question\bank;
|
||||
|
||||
use coding_exception;
|
||||
|
||||
/**
|
||||
* Subclass to customise the view of the question bank for the quiz editing screen.
|
||||
*
|
||||
@@ -58,32 +56,77 @@ class custom_view extends \core_question\local\bank\view {
|
||||
$this->quiz = $quiz;
|
||||
}
|
||||
|
||||
protected function wanted_columns(): array {
|
||||
$quizquestionbankcolumns = [
|
||||
'mod_quiz\\question\\bank\\add_action_column',
|
||||
'core_question\\local\\bank\\checkbox_column',
|
||||
'qbank_viewquestiontype\\question_type_column',
|
||||
'mod_quiz\\question\\bank\\question_name_text_column',
|
||||
'qbank_previewquestion\\preview_action_column',
|
||||
protected function get_question_bank_plugins(): array {
|
||||
$questionbankclasscolumns = [];
|
||||
$corequestionbankcolumns = [
|
||||
'add_action_column',
|
||||
'checkbox_column',
|
||||
'question_type_column',
|
||||
'question_name_text_column',
|
||||
'preview_action_column'
|
||||
];
|
||||
|
||||
foreach ($quizquestionbankcolumns as $fullname) {
|
||||
if (!class_exists($fullname)) {
|
||||
throw new coding_exception('Invalid quiz question bank column', $fullname);
|
||||
}
|
||||
$this->requiredcolumns[$fullname] = new $fullname($this);
|
||||
if (question_get_display_preference('qbshowtext', 0, PARAM_BOOL, new \moodle_url(''))) {
|
||||
$corequestionbankcolumns[] = 'question_text_row';
|
||||
}
|
||||
return $this->requiredcolumns;
|
||||
|
||||
foreach ($corequestionbankcolumns as $fullname) {
|
||||
$shortname = $fullname;
|
||||
if (class_exists('mod_quiz\\question\\bank\\' . $fullname)) {
|
||||
$fullname = 'mod_quiz\\question\\bank\\' . $fullname;
|
||||
$questionbankclasscolumns[$shortname] = new $fullname($this);
|
||||
} else if (class_exists('core_question\\local\\bank\\' . $fullname)) {
|
||||
$fullname = 'core_question\\local\\bank\\' . $fullname;
|
||||
$questionbankclasscolumns[$shortname] = new $fullname($this);
|
||||
} else {
|
||||
$questionbankclasscolumns[$shortname] = '';
|
||||
}
|
||||
}
|
||||
$plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
|
||||
foreach ($plugins as $componentname => $plugin) {
|
||||
$pluginentrypointobject = new $plugin();
|
||||
$plugincolumnobjects = $pluginentrypointobject->get_question_columns($this);
|
||||
// Don't need the plugins without column objects.
|
||||
if (empty($plugincolumnobjects)) {
|
||||
unset($plugins[$componentname]);
|
||||
continue;
|
||||
}
|
||||
foreach ($plugincolumnobjects as $columnobject) {
|
||||
$columnname = $columnobject->get_column_name();
|
||||
foreach ($corequestionbankcolumns as $key => $corequestionbankcolumn) {
|
||||
if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
|
||||
unset($questionbankclasscolumns[$columnname]);
|
||||
continue;
|
||||
}
|
||||
// Check if it has custom preference selector to view/hide.
|
||||
if ($columnobject->has_preference() && !$columnobject->get_preference()) {
|
||||
continue;
|
||||
}
|
||||
if ($corequestionbankcolumn === $columnname) {
|
||||
$questionbankclasscolumns[$columnname] = $columnobject;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mitigate the error in case of any regression.
|
||||
foreach ($questionbankclasscolumns as $shortname => $questionbankclasscolumn) {
|
||||
if (empty($questionbankclasscolumn)) {
|
||||
unset($questionbankclasscolumns[$shortname]);
|
||||
}
|
||||
}
|
||||
|
||||
return $questionbankclasscolumns;
|
||||
}
|
||||
|
||||
protected function heading_column(): string {
|
||||
return 'mod_quiz\\question\\bank\\question_name_text_column';
|
||||
return 'question_name_text_column';
|
||||
}
|
||||
|
||||
protected function default_sort(): array {
|
||||
return [
|
||||
'qbank_viewquestiontype\\question_type_column' => 1,
|
||||
'mod_quiz\\question\\bank\\question_name_text_column' => 1,
|
||||
'question_type_column' => 1,
|
||||
'question_name_text_column' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ Feature: Edit quiz page - adding things
|
||||
And I open the "last" add to quiz menu
|
||||
And I follow "from question bank"
|
||||
Then the "Add selected questions to the quiz" "button" should be disabled
|
||||
And I click on "Essay 03" "checkbox"
|
||||
And I click on "Select" "checkbox" in the "Essay 03" "table_row"
|
||||
Then the "Add selected questions to the quiz" "button" should be enabled
|
||||
And I click on "Add to quiz" "link" in the "Essay 03" "table_row"
|
||||
Then I should see "Editing quiz: Quiz 1"
|
||||
|
||||
@@ -121,7 +121,11 @@ Feature: Adding questions to a quiz from the question bank
|
||||
And I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
|
||||
When I open the "last" add to quiz menu
|
||||
And I follow "from question bank"
|
||||
And I click on "Sort by Question ascending" "link"
|
||||
Then "question 01 name" "text" should appear before "question 02 name" "text"
|
||||
And I click on "Sort by Question descending" "link"
|
||||
And "question 03 name" "text" should appear before "question 01 name" "text"
|
||||
And I follow "Sort by Question type ascending"
|
||||
Then "question 01 name" "text" should appear before "question 03 name" "text"
|
||||
And I follow "Sort by Question type descending"
|
||||
Then "question 03 name" "checkbox" should appear before "question 01 name" "checkbox"
|
||||
And I follow "Sort by Question ascending"
|
||||
And "question 01 name" "checkbox" should appear before "question 02 name" "checkbox"
|
||||
Then "question 03 name" "text" should appear before "question 01 name" "text"
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
/**
|
||||
* Provides the information to backup question custom field.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class backup_qbank_customfields_plugin extends \backup_qbank_plugin {
|
||||
|
||||
/**
|
||||
* Returns the comment information to attach to question element.
|
||||
*
|
||||
* @return backup_plugin_element The backup plugin element
|
||||
*/
|
||||
protected function define_question_plugin_structure(): backup_plugin_element {
|
||||
|
||||
// Define the virtual plugin element with the condition to fulfill.
|
||||
$plugin = $this->get_plugin_element();
|
||||
|
||||
// Create one standard named plugin element (the visible container).
|
||||
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
|
||||
|
||||
// Connect the visible container ASAP.
|
||||
$plugin->add_child($pluginwrapper);
|
||||
|
||||
$customfields = new backup_nested_element('customfields');
|
||||
$customfield = new backup_nested_element('customfield', ['id'], ['shortname', 'type', 'value', 'valueformat']);
|
||||
|
||||
$pluginwrapper->add_child($customfields);
|
||||
$customfields->add_child($customfield);
|
||||
|
||||
$customfield->set_source_sql("SELECT cfd.id, cff.shortname, cff.type, cfd.value, cfd.valueformat
|
||||
FROM {customfield_data} cfd
|
||||
JOIN {customfield_field} cff ON cff.id = cfd.fieldid
|
||||
JOIN {customfield_category} cfc ON cfc.id = cff.categoryid
|
||||
WHERE cfc.component = 'qbank_customfields'
|
||||
AND cfc.area = 'question'
|
||||
AND cfd.instanceid = ?",
|
||||
[
|
||||
backup::VAR_PARENTID
|
||||
]);
|
||||
|
||||
// Don't need to annotate ids nor files.
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Restore plugin class that provides the necessary information needed to restore comments for questions.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class restore_qbank_customfields_plugin extends restore_qbank_plugin {
|
||||
|
||||
/**
|
||||
* Returns the paths to be handled by the plugin at question level.
|
||||
*
|
||||
* @return restore_path_element[] The restore path element array.
|
||||
*/
|
||||
protected function define_question_plugin_structure(): array {
|
||||
return [new restore_path_element('customfield', $this->get_pathfor('/customfields/customfield'))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the question custom field element.
|
||||
*
|
||||
* @param array $data The custom field data to restore.
|
||||
*/
|
||||
public function process_customfield(array $data) {
|
||||
global $DB;
|
||||
|
||||
$newquestion = $this->get_new_parentid('question');
|
||||
|
||||
if (!empty($data->contextid) && $newcontextid = $this->get_mappingid('context', $data->contextid)) {
|
||||
$fieldcontextid = $newcontextid;
|
||||
} else {
|
||||
// Get the category, so we can then later get the context.
|
||||
$categoryid = $this->get_new_parentid('question_category');
|
||||
if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
|
||||
$this->cachedcategory = $DB->get_record('question_categories', ['id' => $categoryid]);
|
||||
}
|
||||
$fieldcontextid = $this->cachedcategory->contextid;
|
||||
}
|
||||
|
||||
$data['newquestion'] = $newquestion;
|
||||
$data['fieldcontextid'] = $fieldcontextid;
|
||||
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$customfieldhandler->restore_instance_data_from_backup($this->task, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?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 qbank_customfields;
|
||||
|
||||
use core_question\local\bank\column_base;
|
||||
|
||||
/**
|
||||
* A column type for the name of the question creator.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class custom_field_column extends column_base {
|
||||
|
||||
/** @var \core_customfield\field_controller The custom field this column is displaying. */
|
||||
protected $field;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param view $qbank the question bank view we are helping to render.
|
||||
* @param \core_customfield\field_controller $field The custom field this column is displaying.
|
||||
*/
|
||||
public function __construct(\core_question\local\bank\view $qbank, \core_customfield\field_controller $field) {
|
||||
parent::__construct($qbank);
|
||||
$this->field = $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'customfield';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(): string {
|
||||
return 'custom_field_column\\' . $this->field->get('shortname');
|
||||
}
|
||||
|
||||
/**
|
||||
* Title for this column. Not used if is_sortable returns an array.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_title(): string {
|
||||
return $this->field->get_formatted_name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
protected function display_content($question, $rowclasses): void {
|
||||
$fieldhandler = $this->field->get_handler();
|
||||
if ($fieldhandler->can_view($this->field, $question->id)) {
|
||||
$fielddata = $fieldhandler->get_field_data($this->field, $question->id);
|
||||
echo $fieldhandler->display_custom_field_table($fielddata);
|
||||
} else {
|
||||
echo '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
<?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 qbank_customfields\customfield;
|
||||
|
||||
use core_customfield\api;
|
||||
use core_customfield\field_controller;
|
||||
use core_customfield\output\field_data;
|
||||
|
||||
/**
|
||||
* Question handler for custom fields.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_handler extends \core_customfield\handler {
|
||||
|
||||
/**
|
||||
* @var question_handler
|
||||
*/
|
||||
static protected $singleton;
|
||||
|
||||
/**
|
||||
* @var \context
|
||||
*/
|
||||
protected $parentcontext;
|
||||
|
||||
/** @var int Field is displayed in the question display and question preview, visible to everybody */
|
||||
const VISIBLETOALL = 2;
|
||||
/** @var int Field is displayed in the question display and question preview but only for "teachers" */
|
||||
const VISIBLETOTEACHERS = 1;
|
||||
/** @var int Field is not displayed in the question display and question preview */
|
||||
const NOTVISIBLE = 0;
|
||||
|
||||
/**
|
||||
* Creates the custom field handler and returns a singleton.
|
||||
* Itemid is always zero as the custom fields are the same
|
||||
* for every question across the system.
|
||||
*
|
||||
* @param int $itemid Always zero.
|
||||
* @return \qbank_customfields\customfield\question_handler
|
||||
*/
|
||||
public static function create(int $itemid = 0) : \core_customfield\handler {
|
||||
if (static::$singleton === null) {
|
||||
self::$singleton = new static(0);
|
||||
}
|
||||
return self::$singleton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run reset code after unit tests to reset the singleton usage.
|
||||
*/
|
||||
public static function reset_caches(): void {
|
||||
if (!PHPUNIT_TEST) {
|
||||
throw new \coding_exception('This feature is only intended for use in unit tests');
|
||||
}
|
||||
|
||||
static::$singleton = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current user can configure custom fields on this component.
|
||||
*
|
||||
* @return bool true if the current can configure custom fields, false otherwise
|
||||
*/
|
||||
public function can_configure() : bool {
|
||||
return has_capability('qbank/customfields:configurecustomfields', $this->get_configuration_context());
|
||||
}
|
||||
|
||||
/**
|
||||
* The current user can edit custom fields for the given question.
|
||||
*
|
||||
* @param field_controller $field
|
||||
* @param int $instanceid id of the question to test edit permission
|
||||
* @return bool true if the current can edit custom fields, false otherwise
|
||||
*/
|
||||
public function can_edit(field_controller $field, int $instanceid = 0) : bool {
|
||||
if ($instanceid) {
|
||||
$context = $this->get_instance_context($instanceid);
|
||||
} else {
|
||||
$context = $this->get_parent_context();
|
||||
}
|
||||
|
||||
return (!$field->get_configdata_property('locked') ||
|
||||
has_capability('qbank/customfields:changelockedcustomfields', $context));
|
||||
}
|
||||
|
||||
/**
|
||||
* The current user can view custom fields for the given question.
|
||||
*
|
||||
* @param field_controller $field
|
||||
* @param int $instanceid id of the question to test edit permission
|
||||
* @return bool true if the current can edit custom fields, false otherwise
|
||||
*/
|
||||
public function can_view(field_controller $field, int $instanceid) : bool {
|
||||
$visibility = $field->get_configdata_property('visibility');
|
||||
if ($visibility == self::NOTVISIBLE) {
|
||||
return false;
|
||||
} else if ($visibility == self::VISIBLETOTEACHERS) {
|
||||
return has_capability('qbank/customfields:viewhiddencustomfields', $this->get_instance_context($instanceid));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current user can view custom field in their given context.
|
||||
* This determines if the user can see the field at all not just the field for
|
||||
* a particular instance.
|
||||
* Used primarily in showing or not the field in the question bank table.
|
||||
*
|
||||
* @param field_controller $field The field trying to be viewed.
|
||||
* @param context $context The context the field is being displayed in.
|
||||
* @return bool true if the current can edit custom fields, false otherwise.
|
||||
*/
|
||||
public function can_view_type(field_controller $field, \context $context) : bool {
|
||||
$visibility = $field->get_configdata_property('visibility');
|
||||
if ($visibility == self::NOTVISIBLE) {
|
||||
return false;
|
||||
} else if ($visibility == self::VISIBLETOTEACHERS) {
|
||||
return has_capability('qbank/customfields:viewhiddencustomfields', $context);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets parent context for the question.
|
||||
*
|
||||
* This may be needed when question is being created, there is no question context but we need to check capabilities
|
||||
*
|
||||
* @param \context $context
|
||||
*/
|
||||
public function set_parent_context(\context $context): void {
|
||||
$this->parentcontext = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent context for the question.
|
||||
*
|
||||
* @return \context
|
||||
*/
|
||||
protected function get_parent_context() : \context {
|
||||
if ($this->parentcontext) {
|
||||
return $this->parentcontext;
|
||||
} else {
|
||||
return \context_system::instance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context that should be used for new categories created by this handler.
|
||||
*
|
||||
* @return \context the context for configuration
|
||||
*/
|
||||
public function get_configuration_context() : \context {
|
||||
return \context_system::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* URL for configuration page for the fields for the question custom fields.
|
||||
*
|
||||
* @return \moodle_url The URL to configure custom fields for this component
|
||||
*/
|
||||
public function get_configuration_url() : \moodle_url {
|
||||
return new \moodle_url('/question/customfield.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context for the data associated with the given instanceid.
|
||||
*
|
||||
* @param int $instanceid id of the record to get the context for
|
||||
* @return \context the context for the given record
|
||||
* @throws \coding_exception
|
||||
*/
|
||||
public function get_instance_context(int $instanceid = 0) : \context {
|
||||
if ($instanceid > 0) {
|
||||
$questiondata = \question_bank::load_question_data($instanceid);
|
||||
$contextid = $questiondata->contextid;
|
||||
$context = \context::instance_by_id($contextid);
|
||||
return $context;
|
||||
} else {
|
||||
throw new \coding_exception('Instance id must be provided.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a field and instance id get all the filed data.
|
||||
*
|
||||
* @param field_controller $field The field to get the data for.
|
||||
* @param int $instanceid The instance id to get the data for.
|
||||
* @return \core_customfield\data_controller The fetched data.
|
||||
*/
|
||||
public function get_field_data(\core_customfield\field_controller $field, int $instanceid): \core_customfield\data_controller {
|
||||
$fields = [$field->get('id') => $field];
|
||||
$fieldsdata = api::get_instance_fields_data($fields, $instanceid);
|
||||
return $fieldsdata[$field->get('id')];
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given instance id (question id) get the categories and the
|
||||
* fields with any data. Return an array of categories containing an
|
||||
* array of field names and values that is ready to be passed to a renderer.
|
||||
*
|
||||
* @param int $instanceid The instance id to get the data for.
|
||||
* @return array $cfdata The fetched data
|
||||
*/
|
||||
public function get_categories_fields_data(int $instanceid): array {
|
||||
// Prepare custom fields data.
|
||||
$instancedata = $this->get_instance_data($instanceid);
|
||||
|
||||
$cfdata = [];
|
||||
|
||||
foreach ($instancedata as $instance) {
|
||||
$field = $instance->get_field();
|
||||
|
||||
if ($this->can_view($field, $instanceid)) {
|
||||
$category = $instance->get_field()->get_category()->get('name');
|
||||
$fieldname = $field->get_formatted_name();
|
||||
$fieldvalue = $this->get_field_data($field, $instanceid)->export_value();
|
||||
$cfdata[$category][] = ['name' => $fieldname, 'value' => $fieldvalue];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $cfdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom data for the given field
|
||||
* and render HTML ready for display in question table.
|
||||
*
|
||||
* @param object $fielddata The field data used for display.
|
||||
* @return string The HTML to display in the table column.
|
||||
*/
|
||||
public function display_custom_field_table(object $fielddata) : string {
|
||||
global $PAGE;
|
||||
|
||||
$output = $PAGE->get_renderer('qbank_customfields');
|
||||
$outputdata = new field_data($fielddata);
|
||||
|
||||
return $output->render_for_table($outputdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the custom field category and filed data as HTML ready for display.
|
||||
*
|
||||
* @param array $catfielddata Array of categories and field names and values.
|
||||
* @return string The HTML to display.
|
||||
*/
|
||||
public function display_custom_categories_fields(array $catfielddata) : string {
|
||||
global $PAGE;
|
||||
$output = $PAGE->get_renderer('qbank_customfields');
|
||||
|
||||
return $output->render_for_preview($catfielddata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom controls to the field configuration form that will be saved.
|
||||
*
|
||||
* @param \MoodleQuickForm $mform The form to add the custom fields to.
|
||||
*/
|
||||
public function config_form_definition(\MoodleQuickForm $mform): void {
|
||||
$mform->addElement('header', 'question_handler_header',
|
||||
get_string('customfieldsettings', 'qbank_customfields'));
|
||||
$mform->setExpanded('question_handler_header', true);
|
||||
|
||||
// If field is locked.
|
||||
$mform->addElement('selectyesno', 'configdata[locked]',
|
||||
get_string('customfield_islocked', 'qbank_customfields'));
|
||||
$mform->addHelpButton('configdata[locked]', 'customfield_islocked', 'qbank_customfields');
|
||||
|
||||
// Field data visibility.
|
||||
$visibilityoptions = [
|
||||
self::VISIBLETOALL => get_string('customfield_visibletoall', 'qbank_customfields'),
|
||||
self::VISIBLETOTEACHERS => get_string('customfield_visibletoteachers', 'qbank_customfields'),
|
||||
self::NOTVISIBLE => get_string('customfield_notvisible', 'qbank_customfields')
|
||||
];
|
||||
$mform->addElement('select', 'configdata[visibility]',
|
||||
get_string('customfield_visibility', 'qbank_customfields'),
|
||||
$visibilityoptions);
|
||||
$mform->addHelpButton(
|
||||
'configdata[visibility]', 'customfield_visibility', 'qbank_customfields');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates the question custom field data when restoring from a backup.
|
||||
*
|
||||
* @param \restore_task $task
|
||||
* @param array $data
|
||||
*/
|
||||
public function restore_instance_data_from_backup(\restore_task $task, array $data): void {
|
||||
|
||||
$editablefields = $this->get_editable_fields($data['newquestion']);
|
||||
$records = api::get_instance_fields_data($editablefields, $data['newquestion']);
|
||||
$target = $task->get_target();
|
||||
$override = ($target != \backup::TARGET_CURRENT_ADDING && $target != \backup::TARGET_EXISTING_ADDING);
|
||||
|
||||
foreach ($records as $d) {
|
||||
$field = $d->get_field();
|
||||
if ($field->get('shortname') === $data['shortname'] && $field->get('type') === $data['type']) {
|
||||
if (!$d->get('id') || $override) {
|
||||
$d->set($d->datafield(), $data['value']);
|
||||
$d->set('value', $data['value']);
|
||||
$d->set('valueformat', $data['valueformat']);
|
||||
$d->set('contextid', $data['fieldcontextid']);
|
||||
$d->save();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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 qbank_customfields\output;
|
||||
|
||||
/**
|
||||
* Class renderer.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class renderer extends \plugin_renderer_base {
|
||||
|
||||
/**
|
||||
* Render custom field data for table display.
|
||||
*
|
||||
* @param object $fielddata The field data to display.
|
||||
* @return string The rendered HTML.
|
||||
*/
|
||||
public function render_for_table(object $fielddata): string {
|
||||
$context = $fielddata->export_for_template($this);
|
||||
return $this->render_from_template('qbank_customfields/table_display', $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom filed data for table display.
|
||||
*
|
||||
* @param array $catfielddata The category and field data.
|
||||
* @return string The rendered HTML.
|
||||
*/
|
||||
public function render_for_preview(array $catfielddata): string {
|
||||
$context = ['categories' => []];
|
||||
|
||||
foreach ($catfielddata as $key => $value) {
|
||||
$context['categories'][] = [
|
||||
'catname' => $key,
|
||||
'fields' => $value
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
return $this->render_from_template('qbank_customfields/preview_display', $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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 qbank_customfields;
|
||||
|
||||
use core_question\local\bank\plugin_features_base;
|
||||
use core_question\local\bank\view;
|
||||
use qbank_customfields\customfield\question_handler;
|
||||
|
||||
/**
|
||||
* Class plugin_feature is the entrypoint for the columns.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Ghaly Marc-Alexandre <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class plugin_feature extends plugin_features_base {
|
||||
|
||||
/**
|
||||
* This method will return the array of objects to be rendered as a prt of question bank columns/actions.
|
||||
*
|
||||
* @param view $qbank
|
||||
* @return array
|
||||
*/
|
||||
public function get_question_columns(view $qbank): array {
|
||||
// We make a column for each custom field and load the data into it.
|
||||
$columns = [];
|
||||
|
||||
// First get all the available question custom fields.
|
||||
$customfieldhandler = question_handler::create();
|
||||
$fields = $customfieldhandler->get_fields();
|
||||
$context = $qbank->get_most_specific_context();
|
||||
|
||||
// Iterate through the fields initialising a column for each.
|
||||
// We don't need to know the values that questions have at this stage.
|
||||
foreach ($fields as $field) {
|
||||
if ($customfieldhandler->can_view_type($field, $context)) {
|
||||
$customfieldcolumn = new custom_field_column($qbank, $field);
|
||||
$columns[] = $customfieldcolumn;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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 qbank_customfields\privacy;
|
||||
|
||||
/**
|
||||
* Privacy Subsystem for qbank_customfields implementing null_provider.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\null_provider {
|
||||
|
||||
/**
|
||||
* Get the language string identifier with the component's language
|
||||
* file to explain why this plugin stores no data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_reason(): string {
|
||||
return 'privacy:metadata';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Manage question custom fields
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once('../../..//config.php');
|
||||
require_once($CFG->libdir.'/adminlib.php');
|
||||
|
||||
admin_externalpage_setup('qbank_customfields');
|
||||
|
||||
$output = $PAGE->get_renderer('core_customfield');
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$outputpage = new \core_customfield\output\management($customfieldhandler);
|
||||
|
||||
echo $output->header(),
|
||||
$output->heading(new lang_string('pluginname', 'qbank_customfields')),
|
||||
$output->render($outputpage),
|
||||
$output->footer();
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Plugin capabilities are defined here.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @category access
|
||||
* @copyright 2021 Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$capabilities = [
|
||||
// Controls if a user can can change the value of a locked custom field.
|
||||
'qbank/customfields:changelockedcustomfields' => [
|
||||
'riskbitmask' => RISK_SPAM,
|
||||
'captype' => 'write',
|
||||
'contextlevel' => CONTEXT_COURSE,
|
||||
'archetypes' => [
|
||||
'manager' => CAP_ALLOW
|
||||
],
|
||||
],
|
||||
|
||||
// Controls if a user can configure question custom fields.
|
||||
'qbank/customfields:configurecustomfields' => [
|
||||
'riskbitmask' => RISK_SPAM,
|
||||
'captype' => 'write',
|
||||
'contextlevel' => CONTEXT_SYSTEM,
|
||||
'clonepermissionsfrom' => 'moodle/site:config'
|
||||
],
|
||||
|
||||
// Controls if a user can view the content of hidden custom fields.
|
||||
'qbank/customfields:viewhiddencustomfields' => [
|
||||
'captype' => 'read',
|
||||
'contextlevel' => CONTEXT_COURSE,
|
||||
'archetypes' => [
|
||||
'editingteacher' => CAP_ALLOW,
|
||||
'manager' => CAP_ALLOW
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Strings for component qbank_customfields, language 'en'.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['pluginname'] = 'Question custom fields';
|
||||
$string['privacy:metadata'] = 'Question custom fields plugin does not store any user data.';
|
||||
|
||||
$string['customfield'] = 'Question custom fields';
|
||||
$string['customfield_islocked'] = 'Locked';
|
||||
$string['customfield_islocked_help'] = 'If the field is locked, only users with the capability to change locked custom fields will be able to change it in the question settings.';
|
||||
$string['customfield_notvisible'] = 'Nobody';
|
||||
$string['customfield_visibility'] = 'Visible to';
|
||||
$string['customfield_visibility_help'] = 'This setting determines who can view the custom field name and value.';
|
||||
$string['customfield_visibletoall'] = 'Everyone';
|
||||
$string['customfield_visibletoteachers'] = 'Teachers';
|
||||
$string['customfieldsettings'] = 'Common question custom fields settings';
|
||||
$string['customfields:changelockedcustomfields'] = 'Change locked custom fields';
|
||||
$string['customfields:configurecustomfields'] = 'Configure custom fields';
|
||||
$string['customfields:viewhiddencustomfields'] = 'View hidden custom fields';
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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 functions and callbacks.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Comment content for callbacks.
|
||||
*
|
||||
* @param question_definition $question
|
||||
* @param int $courseid
|
||||
* @return string
|
||||
*/
|
||||
function qbank_customfields_preview_display(question_definition $question, int $courseid): string {
|
||||
// Prepare custom fields data.
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$catfielddata = $customfieldhandler->get_categories_fields_data($question->id);
|
||||
return $customfieldhandler->display_custom_categories_fields($catfielddata);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Plugin administration pages are defined here.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
if ($hassiteconfig) {
|
||||
// Settings for question custom fields.
|
||||
$settings = null;
|
||||
$ADMIN->add('qbanksettings',
|
||||
new admin_externalpage('qbank_customfields',
|
||||
new lang_string('pluginname', 'qbank_customfields'),
|
||||
$CFG->wwwroot . '/question/bank/customfields/customfield.php',
|
||||
['qbank/customfields:configurecustomfields']
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_customfields/preview_display
|
||||
|
||||
Example context (json):
|
||||
{"categories":[
|
||||
{ "catname":"Other fields",
|
||||
"fields":[
|
||||
{"name":"checkit", "value":"No"},
|
||||
{"name":"datetime", "value":"1 October 2021"}
|
||||
]
|
||||
}
|
||||
]}
|
||||
}}
|
||||
<h3>{{#str}} customfield, qbank_customfields {{/str}}</h3>
|
||||
{{#categories}}
|
||||
<h4>{{catname}}</h4>
|
||||
{{#fields}}
|
||||
<div class="customfield customfield_{{type}} customfield_{{shortname}}">
|
||||
<span class="customfieldname">{{{name}}}</span><span class="customfieldseparator">: </span><span class="customfieldvalue">{{{value}}}</span>
|
||||
</div>
|
||||
{{/fields}}
|
||||
{{/categories}}
|
||||
@@ -0,0 +1,31 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_customfields/table_display
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"ftype" : "text",
|
||||
"shortname" : "nickname",
|
||||
"value" : "Star Lord"
|
||||
}
|
||||
}}
|
||||
{{#hasvalue}}
|
||||
<div class="customfield customfield_{{type}} customfield_{{shortname}}">
|
||||
<span class="customfieldvalue">{{{value}}}</span>
|
||||
</div>
|
||||
{{/hasvalue}}
|
||||
@@ -0,0 +1,60 @@
|
||||
@core @core_question @core_customfield @qbank_customfields @javascript
|
||||
Feature: A teacher can edit question with custom fields
|
||||
In order to improve my questions
|
||||
As a teacher
|
||||
I need to be able to edit questions and add extra metadata via custom fields
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | weeks |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "custom field categories" exist:
|
||||
| name | component | area | itemid |
|
||||
| Category for test | qbank_customfields | question | 0 |
|
||||
And the following "custom fields" exist:
|
||||
| name | category | type | shortname |
|
||||
| Field 1 | Category for test | text | f1 |
|
||||
And the following "activity" exists:
|
||||
| activity | quiz |
|
||||
| course | C1 |
|
||||
| idnumber | 00001 |
|
||||
| name | Test quiz name |
|
||||
| intro | Test quiz description |
|
||||
| section | 1 |
|
||||
| grade | 10 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "True/False" question to the "Test quiz name" quiz with:
|
||||
| Question name | First question |
|
||||
| Question text | Answer the first question |
|
||||
| General feedback | Thank you, this is the general feedback |
|
||||
| Correct answer | False |
|
||||
| Feedback for the response 'True'. | So you think it is true |
|
||||
| Feedback for the response 'False'. | So you think it is false |
|
||||
And I am on the "Test quiz name" "quiz activity" page
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
|
||||
Scenario: Edit a previously created question and see the custom field in the overview table and in the question preview.
|
||||
When I choose "Edit question" action for "First question" in the question bank
|
||||
And I should see "Category for test"
|
||||
And I click on "Expand all" "link"
|
||||
And I should see "Field 1"
|
||||
And I set the following fields to these values:
|
||||
| Field 1 | custom field text |
|
||||
And I press "id_submitbutton"
|
||||
And I should see "First question"
|
||||
And I should see "custom field text"
|
||||
And I choose "Preview" action for "First question" in the question bank
|
||||
And I should see "Field 1"
|
||||
Then I should see "custom field text"
|
||||
|
||||
Scenario: Preview a previously created question with custom fields set with empty values
|
||||
When I choose "Preview" action for "First question" in the question bank
|
||||
And I should see "Field 1"
|
||||
Then I should not see "custom field text"
|
||||
@@ -0,0 +1,98 @@
|
||||
@core @core_question @core_customfield @qbank_customfields @javascript
|
||||
Feature: Site administrators can manage categories for question custom fields
|
||||
In order to have additional data in questions
|
||||
As a site site administrator
|
||||
I need to create, edit, remove and sort question custom field categories
|
||||
|
||||
Scenario: Create a category for custom question fields
|
||||
Given I log in as "admin"
|
||||
When I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
|
||||
And I press "Add a new category"
|
||||
And I wait until the page is ready
|
||||
Then I should see "Other fields" in the "#customfield_catlist" "css_element"
|
||||
|
||||
Scenario: Edit a category name for custom question fields
|
||||
Given the following "custom field categories" exist:
|
||||
| name | component | area | itemid |
|
||||
| Category for test | qbank_customfields | question | 0 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
|
||||
And I set the field "Edit category name" in the "//div[contains(@class,'categoryinstance') and contains(.,'Category for test')]" "xpath_element" to "Good fields"
|
||||
Then I should not see "Category for test" in the "#customfield_catlist" "css_element"
|
||||
And "New value for Category for test" "field" should not exist
|
||||
And I should see "Good fields" in the "#customfield_catlist" "css_element"
|
||||
|
||||
Scenario: Delete a category for custom question fields
|
||||
Given the following "custom field categories" exist:
|
||||
| name | component | area | itemid |
|
||||
| Category for test | qbank_customfields | question | 0 |
|
||||
And the following "custom fields" exist:
|
||||
| name | category | type | shortname |
|
||||
| Field 1 | Category for test | text | f1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
|
||||
And I click on "[data-role='deletecategory']" "css_element"
|
||||
And I click on "Yes" "button" in the "Confirm" "dialogue"
|
||||
And I wait until the page is ready
|
||||
And I wait until "Test category" "text" does not exist
|
||||
Then I should not see "Test category" in the "#customfield_catlist" "css_element"
|
||||
|
||||
Scenario: Move field in the question custom fields to another category
|
||||
Given the following "custom field categories" exist:
|
||||
| name | component | area | itemid |
|
||||
| Category1 | qbank_customfields | question | 0 |
|
||||
| Category2 | qbank_customfields | question | 0 |
|
||||
| Category3 | qbank_customfields | question | 0 |
|
||||
And the following "custom fields" exist:
|
||||
| name | category | type | shortname |
|
||||
| Field1 | Category1 | text | f1 |
|
||||
| Field2 | Category2 | text | f2 |
|
||||
When I log in as "admin"
|
||||
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
|
||||
Then "Field1" "text" should appear after "Category1" "text"
|
||||
And "Category2" "text" should appear after "Field1" "text"
|
||||
And "Field2" "text" should appear after "Category2" "text"
|
||||
And "Category3" "text" should appear after "Field2" "text"
|
||||
And I press "Move \"Field1\""
|
||||
And I follow "To the top of category Category2"
|
||||
And "Category2" "text" should appear after "Category1" "text"
|
||||
And "Field1" "text" should appear after "Category2" "text"
|
||||
And "Field2" "text" should appear after "Field1" "text"
|
||||
And "Category3" "text" should appear after "Field2" "text"
|
||||
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
|
||||
And "Category2" "text" should appear after "Category1" "text"
|
||||
And "Field1" "text" should appear after "Category2" "text"
|
||||
And "Field2" "text" should appear after "Field1" "text"
|
||||
And "Category3" "text" should appear after "Field2" "text"
|
||||
And I press "Move \"Field1\""
|
||||
And I follow "After field Field2"
|
||||
And "Field1" "text" should appear after "Field2" "text"
|
||||
|
||||
Scenario: Reorder question custom field categories
|
||||
Given the following "custom field categories" exist:
|
||||
| name | component | area | itemid |
|
||||
| Category1 | qbank_customfields | question | 0 |
|
||||
| Category2 | qbank_customfields | question | 0 |
|
||||
| Category3 | qbank_customfields | question | 0 |
|
||||
And the following "custom fields" exist:
|
||||
| name | category | type | shortname |
|
||||
| Field1 | Category1 | text | f1 |
|
||||
When I log in as "admin"
|
||||
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
|
||||
Then "Field1" "text" should appear after "Category1" "text"
|
||||
And "Category2" "text" should appear after "Field1" "text"
|
||||
And "Category3" "text" should appear after "Category2" "text"
|
||||
And I press "Move \"Category2\""
|
||||
And I follow "After \"Category3\""
|
||||
And "Field1" "text" should appear after "Category1" "text"
|
||||
And "Category3" "text" should appear after "Field1" "text"
|
||||
And "Category2" "text" should appear after "Category3" "text"
|
||||
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
|
||||
And "Field1" "text" should appear after "Category1" "text"
|
||||
And "Category3" "text" should appear after "Field1" "text"
|
||||
And "Category2" "text" should appear after "Category3" "text"
|
||||
And I press "Move \"Category2\""
|
||||
And I follow "After \"Category1\""
|
||||
And "Field1" "text" should appear after "Category1" "text"
|
||||
And "Category2" "text" should appear after "Field1" "text"
|
||||
And "Category3" "text" should appear after "Category2" "text"
|
||||
@@ -0,0 +1,67 @@
|
||||
@core @core_quiz @core_customfield @qbank_customfields @javascript @xxpriority
|
||||
Feature: The visibility of question custom fields control where they are displayed
|
||||
In order to display custom fields in a quiz
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Terry1 | Teacher1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | weeks |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "custom field categories" exist:
|
||||
| name | component | area | itemid |
|
||||
| Category for test | qbank_customfields | question | 0 |
|
||||
And the following "custom fields" exist:
|
||||
| name | category | type | shortname | configdata |
|
||||
| Field 1 | Category for test | text | f1 | {"visibility":"2"} |
|
||||
| Field 2 | Category for test | text | f2 | {"visibility":"2"} |
|
||||
| Field 3 | Category for test | text | f3 | {"visibility":"2","defaultvalue":"secret"} |
|
||||
And the following "activity" exists:
|
||||
| activity | quiz |
|
||||
| course | C1 |
|
||||
| idnumber | 00001 |
|
||||
| name | Test quiz name |
|
||||
| intro | Test quiz description |
|
||||
| section | 1 |
|
||||
| grade | 10 |
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "True/False" question to the "Test quiz name" quiz with:
|
||||
| Question name | First question |
|
||||
| Question text | Answer the first question |
|
||||
| General feedback | Thank you, this is the general feedback |
|
||||
| Correct answer | False |
|
||||
| Feedback for the response 'True'. | So you think it is true |
|
||||
| Feedback for the response 'False'. | So you think it is false |
|
||||
And I am on the "Test quiz name" "quiz activity" page
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
|
||||
@javascript
|
||||
Scenario: Display custom question fields to teachers based on their visibility.
|
||||
When I choose "Edit question" action for "First question" in the question bank
|
||||
And I should see "Category for test"
|
||||
And I click on "Expand all" "link"
|
||||
And I should see "Field 1"
|
||||
And I should see "Field 2"
|
||||
And I should see "Field 3"
|
||||
And I set the following fields to these values:
|
||||
| Field 1 | custom field text one|
|
||||
| Field 2 | custom field text two|
|
||||
And I press "id_submitbutton"
|
||||
And I should see "Field 1"
|
||||
And I should see "custom field text one"
|
||||
And I should see "Field 2"
|
||||
And I should see "custom field text two"
|
||||
And I should see "Field 3"
|
||||
And I should see "secret"
|
||||
And I choose "Preview" action for "First question" in the question bank
|
||||
And I should see "Field 1"
|
||||
And I should see "custom field text one"
|
||||
And I should see "Field 2"
|
||||
And I should see "custom field text two"
|
||||
And I should see "Field 3"
|
||||
Then I should see "secret"
|
||||
@@ -0,0 +1,306 @@
|
||||
<?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/>.
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
|
||||
|
||||
/**
|
||||
* Class qbank_customfields_customfield_testcase
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qbank_customfields_customfield_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* @var array Data object for generating a question.
|
||||
*/
|
||||
protected $question1data;
|
||||
|
||||
/**
|
||||
* @var array Data object for generating a question.
|
||||
*/
|
||||
protected $question2data;
|
||||
|
||||
/**
|
||||
* @var component_generator_base Question Generator.
|
||||
*/
|
||||
protected $qgen;
|
||||
|
||||
/**
|
||||
* @var core_course_category Course category.
|
||||
*/
|
||||
protected $category;
|
||||
|
||||
/**
|
||||
* @var stdClass Course object.
|
||||
*/
|
||||
protected $course;
|
||||
|
||||
/**
|
||||
* @var int Timestamp to use in tests.
|
||||
*/
|
||||
protected $testnow = 1632278491;
|
||||
|
||||
/**
|
||||
* Helper to assist with setting up custom fields.
|
||||
* This is creating custom field category and the fields, not adding instance field data.
|
||||
*/
|
||||
protected function setup_custom_fields(): void {
|
||||
|
||||
$dg = self::getDataGenerator();
|
||||
$data = new \stdClass();
|
||||
$data->component = 'qbank_customfields';
|
||||
$data->area = 'question';
|
||||
|
||||
$catid = $dg->create_custom_field_category($data)->get('id');
|
||||
$dg->create_custom_field(['categoryid' => $catid, 'type' => 'text', 'shortname' => 'f1']);
|
||||
$dg->create_custom_field(['categoryid' => $catid, 'type' => 'checkbox', 'shortname' => 'f2']);
|
||||
$dg->create_custom_field(['categoryid' => $catid, 'type' => 'date', 'shortname' => 'f3',
|
||||
'configdata' => ['startyear' => 2000, 'endyear' => 3000, 'includetime' => 1]]);
|
||||
$dg->create_custom_field(['categoryid' => $catid, 'type' => 'select', 'shortname' => 'f4',
|
||||
'configdata' => ['options' => "a\nb\nc"]]);
|
||||
$dg->create_custom_field(['categoryid' => $catid, 'type' => 'textarea', 'shortname' => 'f5']);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to assist with setting up questions used in tests.
|
||||
*/
|
||||
protected function setup_questions(): void {
|
||||
// Question initial set up.
|
||||
$this->category = $this->getDataGenerator()->create_category();
|
||||
$this->course = $this->getDataGenerator()->create_course(['category' => $this->category->id]);
|
||||
$context = context_coursecat::instance($this->category->id);
|
||||
$this->qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$qcat = $this->qgen->create_question_category(['contextid' => $context->id]);
|
||||
|
||||
$this->question1data = [
|
||||
'category' => $qcat->id, 'idnumber' => 'q1',
|
||||
'customfield_f1' => 'some text', 'customfield_f2' => 1,
|
||||
'customfield_f3' => $this->testnow, 'customfield_f4' => 2,
|
||||
'customfield_f5_editor' => ['text' => 'test', 'format' => FORMAT_HTML]];
|
||||
|
||||
$this->question2data = [
|
||||
'category' => $qcat->id, 'idnumber' => 'q2',
|
||||
'customfield_f1' => 'some more text', 'customfield_f2' => 0,
|
||||
'customfield_f3' => $this->testnow, 'customfield_f4' => 1,
|
||||
'customfield_f5_editor' => ['text' => 'test text', 'format' => FORMAT_HTML]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a backup of the course.
|
||||
*
|
||||
* @param stdClass $course The course object.
|
||||
* @return string Unique identifier for this backup.
|
||||
*/
|
||||
protected function backup_course(\stdClass $course): string {
|
||||
global $CFG, $USER;
|
||||
|
||||
// Turn off file logging, otherwise it can't delete the file (Windows).
|
||||
$CFG->backup_file_logger_level = backup::LOG_NONE;
|
||||
|
||||
// Do backup with default settings. MODE_IMPORT means it will just
|
||||
// create the directory and not zip it.
|
||||
$bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
|
||||
backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
|
||||
$USER->id);
|
||||
$backupid = $bc->get_backupid();
|
||||
$bc->execute_plan();
|
||||
$bc->destroy();
|
||||
|
||||
return $backupid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup that has been made earlier.
|
||||
*
|
||||
* @param string $backupid The unique identifier of the backup.
|
||||
* @param string $fullname Full name of the new course that is going to be created.
|
||||
* @param string $shortname Short name of the new course that is going to be created.
|
||||
* @param int $categoryid The course category the backup is going to be restored in.
|
||||
* @return int The new course id.
|
||||
*/
|
||||
protected function restore_course(string $backupid, string $fullname, string $shortname, int $categoryid): int {
|
||||
global $CFG, $USER;
|
||||
|
||||
// Turn off file logging, otherwise it can't delete the file (Windows).
|
||||
$CFG->backup_file_logger_level = backup::LOG_NONE;
|
||||
|
||||
// Do restore to new course with default settings.
|
||||
$newcourseid = restore_dbops::create_new_course($fullname, $shortname, $categoryid);
|
||||
$rc = new restore_controller($backupid, $newcourseid,
|
||||
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
|
||||
backup::TARGET_NEW_COURSE);
|
||||
|
||||
$rc->execute_precheck();
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
return $newcourseid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating questions with custom fields.
|
||||
*/
|
||||
public function test_create_question(): void {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
$this->setup_custom_fields();
|
||||
$this->setup_questions();
|
||||
|
||||
// Create 2 questions.
|
||||
$question1 = $this->qgen->create_question('shortanswer', null, $this->question1data);
|
||||
$question2 = $this->qgen->create_question('shortanswer', null, $this->question2data);
|
||||
|
||||
// Explicitly save the custom field data for the questions, like a form would.
|
||||
$customfieldhandler = \qbank_customfields\customfield\question_handler::create();
|
||||
$this->question1data['id'] = $question1->id;
|
||||
$this->question2data['id'] = $question2->id;
|
||||
$customfieldhandler->instance_form_save((object)$this->question1data);
|
||||
$customfieldhandler->instance_form_save((object)$this->question2data);
|
||||
|
||||
// Get the custom field data associated with these question ids.
|
||||
$q1cfdata = $customfieldhandler->export_instance_data_object($question1->id);
|
||||
$q2cfdata = $customfieldhandler->export_instance_data_object($question2->id);
|
||||
|
||||
$this->assertEquals('some text', $q1cfdata->f1);
|
||||
$this->assertEquals('Yes', $q1cfdata->f2);
|
||||
$this->assertEquals(userdate($this->testnow, get_string('strftimedaydatetime')), $q1cfdata->f3);
|
||||
$this->assertEquals('b', $q1cfdata->f4);
|
||||
$this->assertEquals('test', $q1cfdata->f5);
|
||||
|
||||
$this->assertEquals('some more text', $q2cfdata->f1);
|
||||
$this->assertEquals('No', $q2cfdata->f2);
|
||||
$this->assertEquals(userdate($this->testnow, get_string('strftimedaydatetime')), $q2cfdata->f3);
|
||||
$this->assertEquals('a', $q2cfdata->f4);
|
||||
$this->assertEquals('test text', $q2cfdata->f5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deleting questions with custom fields.
|
||||
*/
|
||||
public function test_delete_question(): void {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
$this->setup_custom_fields();
|
||||
$this->setup_questions();
|
||||
|
||||
// Create 2 questions.
|
||||
$question1 = $this->qgen->create_question('shortanswer', null, $this->question1data);
|
||||
$question2 = $this->qgen->create_question('shortanswer', null, $this->question2data);
|
||||
|
||||
// Explicitly save the custom field data for the questions, like a form would.
|
||||
$customfieldhandler = \qbank_customfields\customfield\question_handler::create();
|
||||
$this->question1data['id'] = $question1->id;
|
||||
$this->question2data['id'] = $question2->id;
|
||||
$customfieldhandler->instance_form_save((object)$this->question1data);
|
||||
$customfieldhandler->instance_form_save((object)$this->question2data);
|
||||
|
||||
// Get the custom field data associated with these question ids.
|
||||
$q1cfdata = $customfieldhandler->export_instance_data_object($question1->id);
|
||||
$q2cfdata = $customfieldhandler->export_instance_data_object($question2->id);
|
||||
|
||||
// Quick check that we have data for the custom fields.
|
||||
$this->assertEquals('some text', $q1cfdata->f1);
|
||||
$this->assertEquals('some more text', $q2cfdata->f1);
|
||||
|
||||
// Delete the questions.
|
||||
question_delete_question($question1->id);
|
||||
question_delete_question($question2->id);
|
||||
|
||||
// Check the custom field data for the questions has also gone.
|
||||
$q1cfdata = $customfieldhandler->export_instance_data_object($question1->id);
|
||||
$q2cfdata = $customfieldhandler->export_instance_data_object($question2->id);
|
||||
|
||||
$this->assertEmpty($q1cfdata->f1);
|
||||
$this->assertEmpty($q2cfdata->f1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test custom fields attached to questions persist
|
||||
* across the backup and restore process.
|
||||
*/
|
||||
public function test_backup_restore(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
$this->setup_custom_fields();
|
||||
$this->setup_questions();
|
||||
|
||||
$courseshortname = $this->course->shortname;
|
||||
$coursefullname = $this->course->fullname;
|
||||
|
||||
// Create 2 questions.
|
||||
$question1 = $this->qgen->create_question('shortanswer', null, $this->question1data);
|
||||
$question2 = $this->qgen->create_question('shortanswer', null, $this->question2data);
|
||||
|
||||
// Explicitly save the custom field data for the questions, like a form would.
|
||||
$customfieldhandler = \qbank_customfields\customfield\question_handler::create();
|
||||
$this->question1data['id'] = $question1->id;
|
||||
$this->question2data['id'] = $question2->id;
|
||||
$customfieldhandler->instance_form_save((object)$this->question1data);
|
||||
$customfieldhandler->instance_form_save((object)$this->question2data);
|
||||
|
||||
// Create a quiz and the questions to that.
|
||||
$quiz = $this->getDataGenerator()->create_module(
|
||||
'quiz', ['course' => $this->course->id, 'name' => 'restored_quiz']);
|
||||
quiz_add_quiz_question($question1->id, $quiz);
|
||||
quiz_add_quiz_question($question2->id, $quiz);
|
||||
|
||||
// Backup the course.
|
||||
$backupid = $this->backup_course($this->course);
|
||||
|
||||
// Now delete everything.
|
||||
delete_course($this->course, false);
|
||||
question_delete_question($question1->id);
|
||||
question_delete_question($question2->id);
|
||||
|
||||
// Check the custom field data for the questions has also gone.
|
||||
$q1cfdata = $customfieldhandler->export_instance_data_object($question1->id);
|
||||
$q2cfdata = $customfieldhandler->export_instance_data_object($question2->id);
|
||||
|
||||
$this->assertEmpty($q1cfdata->f1);
|
||||
$this->assertEmpty($q2cfdata->f1);
|
||||
|
||||
// Restore the backup we had made earlier into a new course.
|
||||
$newcategory = $this->getDataGenerator()->create_category();
|
||||
$this->restore_course($backupid, $coursefullname, $courseshortname . '_2', $newcategory->id);
|
||||
|
||||
// The questions and their associated custom fields should have been restored.
|
||||
$newquestion1 = $DB->get_record('question', ['idnumber' => 'q1']);
|
||||
$newquestion1cfdata = $customfieldhandler->export_instance_data_object($newquestion1->id);
|
||||
$this->assertEquals('some text', $newquestion1cfdata->f1);
|
||||
$this->assertEquals('Yes', $newquestion1cfdata->f2);
|
||||
$this->assertEquals(userdate($this->testnow, get_string('strftimedaydatetime')), $newquestion1cfdata->f3);
|
||||
$this->assertEquals('b', $newquestion1cfdata->f4);
|
||||
$this->assertEquals('test', $newquestion1cfdata->f5);
|
||||
|
||||
$newquestion2 = $DB->get_record('question', ['idnumber' => 'q2']);
|
||||
$newquestion2cfdata = $customfieldhandler->export_instance_data_object($newquestion2->id);
|
||||
$this->assertEquals('some more text', $newquestion2cfdata->f1);
|
||||
$this->assertEquals('No', $newquestion2cfdata->f2);
|
||||
$this->assertEquals(userdate($this->testnow, get_string('strftimedaydatetime')), $newquestion2cfdata->f3);
|
||||
$this->assertEquals('a', $newquestion2cfdata->f4);
|
||||
$this->assertEquals('test text', $newquestion2cfdata->f5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Class qbank_customfields_question_handler_testcase
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qbank_customfields_question_handler_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Question setup helper method.
|
||||
*
|
||||
* @return int The question id.
|
||||
* @throws coding_exception
|
||||
*/
|
||||
protected function setup_question(): int {
|
||||
$category = $this->getDataGenerator()->create_category();
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$context = context_coursecat::instance($category->id);
|
||||
$questioncategory = $questiongenerator->create_question_category(['contextid' => $context->id]);
|
||||
$questiondata = ['category' => $questioncategory->id, 'idnumber' => 'q1'];
|
||||
$question = $questiongenerator->create_question('shortanswer', null, $questiondata);
|
||||
|
||||
return $question->id;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test custom field data.
|
||||
*/
|
||||
public function test_get_field_data(): void {
|
||||
$this->resetAfterTest();
|
||||
$fieldgenerator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
|
||||
|
||||
$instanceid = $this->setup_question();
|
||||
$fieldvalue = 'test field text';
|
||||
|
||||
$categorydata = new \stdClass();
|
||||
$categorydata->component = 'qbank_customfields';
|
||||
$categorydata->area = 'question';
|
||||
$categorydata->name = 'test category';
|
||||
|
||||
$customfieldcatid = $fieldgenerator->create_category($categorydata)->get('id');
|
||||
$field = $fieldgenerator->create_field(['categoryid' => $customfieldcatid, 'type' => 'text', 'shortname' => 'f1']);
|
||||
$fieldgenerator->add_instance_data($field, $instanceid, $fieldvalue);
|
||||
|
||||
// Get the field data.
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$fieldinstancedata = $customfieldhandler->get_field_data($field, $instanceid);
|
||||
|
||||
$this->assertEquals($categorydata->name, $fieldinstancedata->get_field()->get_category()->get('name'));
|
||||
$this->assertEquals($fieldvalue, $fieldinstancedata->get_value());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting custom field data for table display.
|
||||
*/
|
||||
public function test_display_custom_field_table(): void {
|
||||
$this->resetAfterTest();
|
||||
$fieldgenerator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
|
||||
|
||||
$instanceid = $this->setup_question();
|
||||
$fieldvalue = 'test field text';
|
||||
|
||||
$categorydata = new \stdClass();
|
||||
$categorydata->component = 'qbank_customfields';
|
||||
$categorydata->area = 'question';
|
||||
$categorydata->name = 'test category';
|
||||
|
||||
$customfieldcatid = $fieldgenerator->create_category($categorydata)->get('id');
|
||||
$field = $fieldgenerator->create_field(['categoryid' => $customfieldcatid, 'type' => 'text', 'shortname' => 'f1']);
|
||||
$fieldgenerator->add_instance_data($field, $instanceid, $fieldvalue);
|
||||
|
||||
// Get the field data.
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$fieldinstancedata = $customfieldhandler->get_field_data($field, $instanceid);
|
||||
$output = $customfieldhandler->display_custom_field_table($fieldinstancedata);
|
||||
|
||||
$this->assertStringContainsString($fieldvalue, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting categories and field data for a specific instance.
|
||||
*/
|
||||
public function test_get_categories_fields_data(): void {
|
||||
$this->resetAfterTest();
|
||||
$fieldgenerator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
|
||||
|
||||
$instanceid = $this->setup_question();
|
||||
$field1value = 'first field text';
|
||||
$field2value = 'second field text';
|
||||
$field3value = 'third field text';
|
||||
|
||||
$categorydata = new \stdClass();
|
||||
$categorydata->component = 'qbank_customfields';
|
||||
$categorydata->area = 'question';
|
||||
$categorydata->name = 'test category';
|
||||
|
||||
$customfieldcat1id = $fieldgenerator->create_category($categorydata)->get('id');
|
||||
$categorydata->name = 'test category two';
|
||||
$customfieldcat2id = $fieldgenerator->create_category($categorydata)->get('id');
|
||||
|
||||
$field1 = $fieldgenerator->create_field(['categoryid' => $customfieldcat1id, 'type' => 'text', 'shortname' => 'f1']);
|
||||
$fieldgenerator->add_instance_data($field1, $instanceid, $field1value);
|
||||
$field2 = $fieldgenerator->create_field(['categoryid' => $customfieldcat1id, 'type' => 'text', 'shortname' => 'f2']);
|
||||
$fieldgenerator->add_instance_data($field2, $instanceid, $field2value);
|
||||
|
||||
$field3 = $fieldgenerator->create_field(['categoryid' => $customfieldcat2id, 'type' => 'text', 'shortname' => 'f3']);
|
||||
$fieldgenerator->add_instance_data($field3, $instanceid, $field3value);
|
||||
|
||||
// Get the field data.
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$outputdata = $customfieldhandler->get_categories_fields_data($instanceid);
|
||||
|
||||
$this->assertEquals($field1value, $outputdata['test category'][0]['value']);
|
||||
$this->assertEquals($field2value, $outputdata['test category'][1]['value']);
|
||||
$this->assertEquals($field3value, $outputdata['test category two'][0]['value']);
|
||||
|
||||
// While we're here, lets test the rendering of this data.
|
||||
$outputhtml = $customfieldhandler->display_custom_categories_fields($outputdata);
|
||||
|
||||
$this->assertStringContainsString($field1value, $outputhtml);
|
||||
$this->assertStringContainsString($field2value, $outputhtml);
|
||||
$this->assertStringContainsString($field3value, $outputhtml);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Version information for qbank_customfields.
|
||||
*
|
||||
* @package qbank_customfields
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Matt Porritt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'qbank_customfields';
|
||||
$plugin->version = 2021100500;
|
||||
$plugin->requires = 2021052500;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
@@ -219,6 +219,10 @@ if ($cm !== null) {
|
||||
|
||||
$toform->inpopup = $inpopup;
|
||||
|
||||
// Prepare custom fields data.
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$customfieldhandler->instance_form_before_set_data($toform);
|
||||
|
||||
$mform->set_data($toform);
|
||||
|
||||
if ($mform->is_cancelled()) {
|
||||
@@ -281,6 +285,9 @@ if ($mform->is_cancelled()) {
|
||||
context_course::instance($fromform->courseid), $fromform->coursetags, 0);
|
||||
}
|
||||
|
||||
// Update custom fields if there are any of them in the form.
|
||||
$customfieldhandler->instance_form_save($fromform);
|
||||
|
||||
// Purge this question from the cache.
|
||||
question_bank::notify_question_edited($question->id);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class viewquestionname_column_helper extends column_base {
|
||||
|
||||
protected function label_for($question): string {
|
||||
if (is_null($this->checkboxespresent)) {
|
||||
$this->checkboxespresent = $this->qbank->has_column('core_question\local\bank\checkbox_column');
|
||||
$this->checkboxespresent = $this->qbank->has_column('checkbox_column');
|
||||
}
|
||||
if ($this->checkboxespresent) {
|
||||
return 'checkq' . $question->id;
|
||||
|
||||
@@ -232,6 +232,7 @@ abstract class column_base {
|
||||
protected function get_classes(): string {
|
||||
$classes = $this->get_extra_classes();
|
||||
$classes[] = $this->get_name();
|
||||
$classes[] = 'pr-3';
|
||||
return implode(' ', $classes);
|
||||
}
|
||||
|
||||
@@ -243,6 +244,17 @@ abstract class column_base {
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Any extra class names you would like applied to every cell in this column.
|
||||
*
|
||||
|
||||
@@ -263,30 +263,30 @@ class view {
|
||||
$plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
|
||||
foreach ($plugins as $componentname => $plugin) {
|
||||
$pluginentrypointobject = new $plugin();
|
||||
$pluginobjects = $pluginentrypointobject->get_question_columns($this);
|
||||
$plugincolumnobjects = $pluginentrypointobject->get_question_columns($this);
|
||||
// Don't need the plugins without column objects.
|
||||
if (empty($pluginobjects)) {
|
||||
if (empty($plugincolumnobjects)) {
|
||||
unset($plugins[$componentname]);
|
||||
continue;
|
||||
}
|
||||
foreach ($pluginobjects as $pluginobject) {
|
||||
$classname = new \ReflectionClass(get_class($pluginobject));
|
||||
foreach ($plugincolumnobjects as $columnobject) {
|
||||
$columnname = $columnobject->get_column_name();
|
||||
foreach ($corequestionbankcolumns as $key => $corequestionbankcolumn) {
|
||||
if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
|
||||
unset($questionbankclasscolumns[$classname->getShortName()]);
|
||||
unset($questionbankclasscolumns[$columnname]);
|
||||
continue;
|
||||
}
|
||||
// Check if it has custom preference selector to view/hide.
|
||||
if ($pluginobject->has_preference()) {
|
||||
if (!$pluginobject->get_preference()) {
|
||||
if ($columnobject->has_preference()) {
|
||||
if (!$columnobject->get_preference()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ($corequestionbankcolumn === $classname->getShortName()) {
|
||||
$questionbankclasscolumns[$classname->getShortName()] = $pluginobject;
|
||||
if ($corequestionbankcolumn === $columnname) {
|
||||
$questionbankclasscolumns[$columnname] = $columnobject;
|
||||
} else {
|
||||
// Any community plugin for column/action.
|
||||
$newpluginclasscolumns[$classname->getShortName()] = $pluginobject;
|
||||
$newpluginclasscolumns[$columnname] = $columnobject;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,7 +319,7 @@ class view {
|
||||
if (empty($classobject)) {
|
||||
continue;
|
||||
}
|
||||
$this->requiredcolumns[get_class($classobject)] = $classobject;
|
||||
$this->requiredcolumns[$classobject->get_column_name()] = $classobject;
|
||||
}
|
||||
|
||||
return $this->requiredcolumns;
|
||||
@@ -366,9 +366,9 @@ class view {
|
||||
$this->extrarows = [];
|
||||
foreach ($wanted as $column) {
|
||||
if ($column->is_extra_row()) {
|
||||
$this->extrarows[get_class($column)] = $column;
|
||||
$this->extrarows[$column->get_column_name()] = $column;
|
||||
} else {
|
||||
$this->visiblecolumns[get_class($column)] = $column;
|
||||
$this->visiblecolumns[$column->get_column_name()] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,7 +904,7 @@ class view {
|
||||
protected function create_new_question_form($category, $canadd): void {
|
||||
if (\core\plugininfo\qbank::is_plugin_enabled('qbank_editquestion')) {
|
||||
echo editquestion_helper::create_new_question_button($category->id,
|
||||
$this->requiredcolumns['qbank_editquestion\edit_action_column']->editquestionurl->params(), $canadd);
|
||||
$this->requiredcolumns['edit_action_column']->editquestionurl->params(), $canadd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1115,7 +1115,7 @@ class view {
|
||||
*/
|
||||
protected function start_table() {
|
||||
debugging('Function start_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER);
|
||||
echo '<table id="categoryquestions">' . "\n";
|
||||
echo '<table id="categoryquestions" class="table table-responsive">' . "\n";
|
||||
echo "<thead>\n";
|
||||
$this->print_table_headers();
|
||||
echo "</thead>\n";
|
||||
|
||||
@@ -32,7 +32,8 @@ Feature: The questions in the question bank can be sorted in various ways
|
||||
|
||||
@javascript
|
||||
Scenario: The questions can be sorted by idnumber
|
||||
When I follow "Sort by ID number ascending"
|
||||
When I change the window size to "large"
|
||||
And I follow "Sort by ID number ascending"
|
||||
Then "C question 3 name" "checkbox" should appear after "A question 1 name" "checkbox"
|
||||
And I should see "numidnum</c" in the "C question 3 name" "table_row"
|
||||
And I follow "Sort by ID number descending"
|
||||
|
||||
@@ -222,6 +222,11 @@ abstract class question_edit_form extends question_wizard_form {
|
||||
$this->add_tag_fields($mform);
|
||||
}
|
||||
|
||||
// Add custom fields to the form.
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$customfieldhandler->set_parent_context($this->categorycontext); // For question handler only.
|
||||
$customfieldhandler->instance_form_definition($mform, empty($this->question->id) ? 0 : $this->question->id);
|
||||
|
||||
if (!empty($this->question->id)) {
|
||||
$mform->addElement('header', 'createdmodifiedheader',
|
||||
get_string('createdmodifiedheader', 'question'));
|
||||
@@ -285,6 +290,16 @@ abstract class question_edit_form extends question_wizard_form {
|
||||
// By default, do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Tweak the form with values provided by custom fields in use.
|
||||
*/
|
||||
public function definition_after_data() {
|
||||
$mform = $this->_form;
|
||||
|
||||
$customfieldhandler = qbank_customfields\customfield\question_handler::create();
|
||||
$customfieldhandler->instance_form_definition_after_data($mform, empty($this->question->id) ? 0 : $this->question->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the question being edited in a state where it can be previewed?
|
||||
* @return bool whether to show the preview link.
|
||||
@@ -855,6 +870,10 @@ abstract class question_edit_form extends question_wizard_form {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the custom field validation.
|
||||
$customfieldhandler = core_course\customfield\course_handler::create();
|
||||
$errors = array_merge($errors, $customfieldhandler->instance_form_validation($fromform, $files));
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,11 @@ abstract class question_definition {
|
||||
/** @var array of question_hints. */
|
||||
public $hints = array();
|
||||
|
||||
/**
|
||||
* @var array of array of \core_customfield\data_controller objects indexed by fieldid for the questions custom fields.
|
||||
*/
|
||||
public $customfields = array();
|
||||
|
||||
/**
|
||||
* Constructor. Normally to get a question, you call
|
||||
* {@link question_bank::load_question()}, but questions can be created
|
||||
|
||||
@@ -964,6 +964,9 @@ class question_type {
|
||||
}
|
||||
|
||||
$this->initialise_question_hints($question, $questiondata);
|
||||
|
||||
// Add the custom fields.
|
||||
$this->initialise_custom_fields($question, $questiondata);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -990,6 +993,17 @@ class question_type {
|
||||
return question_hint::load_from_record($hint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise question custom fields.
|
||||
* @param question_definition $question the question_definition we are creating.
|
||||
* @param object $questiondata the question data loaded from the database.
|
||||
*/
|
||||
protected function initialise_custom_fields(question_definition $question, $questiondata) {
|
||||
if (!empty($questiondata->customfields)) {
|
||||
$question->customfields = $questiondata->customfields;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the combined feedback fields.
|
||||
* @param question_definition $question the question_definition we are creating.
|
||||
|
||||
Reference in New Issue
Block a user