diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 0c20d6b4159..3d50eb33317 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1939,7 +1939,7 @@ class core_plugin_manager { ), 'qbank' => [ - 'deletequestion', + 'deletequestion', 'importquestions', ], 'qbehaviour' => array( diff --git a/question/bank/importquestions/classes/form/import_form.php b/question/bank/importquestions/classes/form/import_form.php new file mode 100644 index 00000000000..28bab21acad --- /dev/null +++ b/question/bank/importquestions/classes/form/import_form.php @@ -0,0 +1,179 @@ +. + +/** + * Defines the export questions form. + * + * @package qbank_importquestions + * @copyright 2007 Jamie Pratt me@jamiep.org + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbank_importquestions\form; + +defined('MOODLE_INTERNAL') || die(); + +use moodle_exception; +use moodleform; +use stdClass; + +require_once($CFG->libdir . '/formslib.php'); + +/** + * Form to import questions into the question bank. + * + * @copyright 2007 Jamie Pratt me@jamiep.org + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_import_form extends moodleform { + + /** + * Build the form definition. + * + * This adds all the form fields that the manage categories feature needs. + * @throws \coding_exception + */ + protected function definition() { + global $OUTPUT; + + $mform = $this->_form; + + $defaultcategory = $this->_customdata['defaultcategory']; + $contexts = $this->_customdata['contexts']; + + // Choice of import format, with help icons. + $mform->addElement('header', 'fileformat', get_string('fileformat', 'question')); + + $fileformatnames = get_import_export_formats('import'); + $radioarray = []; + $separators = []; + foreach ($fileformatnames as $shortname => $fileformatname) { + $radioarray[] = $mform->createElement('radio', 'format', '', $fileformatname, $shortname); + + $separator = ''; + if (get_string_manager()->string_exists('pluginname_help', 'qformat_' . $shortname)) { + $separator .= $OUTPUT->help_icon('pluginname', 'qformat_' . $shortname); + } + $separator .= '
'; + $separators[] = $separator; + } + + $radioarray[] = $mform->createElement('static', 'makelasthelpiconshowup', ''); + $mform->addGroup($radioarray, "formatchoices", '', $separators, false); + $mform->addRule("formatchoices", null, 'required', null, 'client'); + + // Import options. + $mform->addElement('header', 'general', get_string('general', 'form')); + + $mform->addElement('questioncategory', 'category', get_string('importcategory', 'question'), compact('contexts')); + $mform->setDefault('category', $defaultcategory); + $mform->addHelpButton('category', 'importcategory', 'question'); + + $categorygroup = []; + $categorygroup[] = $mform->createElement('checkbox', 'catfromfile', '', get_string('getcategoryfromfile', 'question')); + $categorygroup[] = $mform->createElement('checkbox', 'contextfromfile', '', get_string('getcontextfromfile', 'question')); + $mform->addGroup($categorygroup, 'categorygroup', '', '', false); + $mform->disabledIf('categorygroup', 'catfromfile', 'notchecked'); + $mform->setDefault('catfromfile', 1); + $mform->setDefault('contextfromfile', 1); + + $matchgrades = []; + $matchgrades['error'] = get_string('matchgradeserror', 'question'); + $matchgrades['nearest'] = get_string('matchgradesnearest', 'question'); + $mform->addElement('select', 'matchgrades', get_string('matchgrades', 'question'), $matchgrades); + $mform->addHelpButton('matchgrades', 'matchgrades', 'question'); + $mform->setDefault('matchgrades', 'error'); + + $mform->addElement('selectyesno', 'stoponerror', get_string('stoponerror', 'question')); + $mform->setDefault('stoponerror', 1); + $mform->addHelpButton('stoponerror', 'stoponerror', 'question'); + + // The file to import. + $mform->addElement('header', 'importfileupload', get_string('importquestions', 'question')); + + $mform->addElement('filepicker', 'newfile', get_string('import')); + $mform->addRule('newfile', null, 'required', null, 'client'); + + // Submit button. + $mform->addElement('submit', 'submitbutton', get_string('import')); + + // Set a template for the format select elements. + $renderer = $mform->defaultRenderer(); + $template = "{help} {element}\n"; + $renderer->setGroupElementTemplate($template, 'format'); + } + + /** + * Checks that a file has been uploaded, and that it is of a plausible type. + * @param array $data the submitted data. + * @param array $errors the errors so far. + * @return array the updated errors. + * @throws moodle_exception + */ + protected function validate_uploaded_file($data, $errors) { + global $CFG; + + if (empty($data['newfile'])) { + $errors['newfile'] = get_string('required'); + return $errors; + } + + $files = $this->get_draft_files('newfile'); + if (!is_array($files) || count($files) < 1) { + $errors['newfile'] = get_string('required'); + return $errors; + } + + if (empty($data['format'])) { + $errors['format'] = get_string('required'); + return $errors; + } + + $formatfile = $CFG->dirroot . '/question/format/' . $data['format'] . '/format.php'; + if (!is_readable($formatfile)) { + throw new moodle_exception('formatnotfound', 'question', '', $data['format']); + } + + require_once($formatfile); + + $classname = 'qformat_' . $data['format']; + $qformat = new $classname(); + + $file = reset($files); + if (!$qformat->can_import_file($file)) { + $a = new stdClass(); + $a->actualtype = $file->get_mimetype(); + $a->expectedtype = $qformat->mime_type(); + $errors['newfile'] = get_string('importwrongfiletype', 'question', $a); + } + + return $errors; + } + + /** + * Validation. + * + * @param array $data + * @param array $files + * @return array the errors that were found + * @throws \dml_exception|\coding_exception|moodle_exception + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + $errors = $this->validate_uploaded_file($data, $errors); + return $errors; + } +} diff --git a/question/bank/importquestions/classes/navigation.php b/question/bank/importquestions/classes/navigation.php new file mode 100644 index 00000000000..7f24eaecf1a --- /dev/null +++ b/question/bank/importquestions/classes/navigation.php @@ -0,0 +1,50 @@ +. + +/** + * Plugin entrypoint for navigation. + * + * @package qbank_importquestions + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbank_importquestions; + +/** + * Class navigation. + * + * @package qbank_importquestions + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class navigation extends \core_question\local\bank\navigation_node_base { + + public function get_navigation_title(): string { + return get_string('import', 'question'); + } + + public function get_navigation_key(): string { + return 'import'; + } + + public function get_navigation_url(): \moodle_url { + return new \moodle_url('/question/bank/importquestions/import.php'); + } + +} diff --git a/question/bank/importquestions/classes/plugin_feature.php b/question/bank/importquestions/classes/plugin_feature.php new file mode 100644 index 00000000000..fc876200366 --- /dev/null +++ b/question/bank/importquestions/classes/plugin_feature.php @@ -0,0 +1,41 @@ +. + +/** + * Plugin entrypoint for qbank. + * + * @package qbank_importquestions + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbank_importquestions; + +/** + * Class plugin_feature. + * + * @package qbank_importquestions + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugin_feature extends \core_question\local\bank\plugin_features_base { + + public function get_navigation_node(): ?object { + return new navigation(); + } +} diff --git a/question/bank/importquestions/classes/privacy/provider.php b/question/bank/importquestions/classes/privacy/provider.php new file mode 100644 index 00000000000..cbf9a49ed34 --- /dev/null +++ b/question/bank/importquestions/classes/privacy/provider.php @@ -0,0 +1,38 @@ +. + +namespace qbank_importquestions\privacy; + +/** + * Privacy Subsystem implementation for qbank_importquestions. + * + * @package qbank_importquestions + * @category privacy + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Marc-Alexandre Ghaly + * @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'; + } +} diff --git a/question/bank/importquestions/import.php b/question/bank/importquestions/import.php new file mode 100644 index 00000000000..91eab596d0e --- /dev/null +++ b/question/bank/importquestions/import.php @@ -0,0 +1,150 @@ +. + +/** + * Defines the import questions form. + * + * @package qbank_importquestions + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot . '/question/editlib.php'); +require_once($CFG->dirroot . '/question/bank/importquestions/classes/form/import_form.php'); +require_once($CFG->dirroot . '/question/format.php'); +require_once($CFG->dirroot . '/question/renderer.php'); + +use qbank_importquestions\form\question_import_form; + +require_login(); +core_question\local\bank\helper::require_plugin_enabled('qbank_importquestions'); +list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) = + question_edit_setup('import', '/question/bank/importquestions/import.php'); + +// Get display strings. +$txt = new stdClass(); +$txt->importerror = get_string('importerror', 'question'); +$txt->importquestions = get_string('importquestions', 'question'); + +list($catid, $catcontext) = explode(',', $pagevars['cat']); +if (!$category = $DB->get_record("question_categories", ['id' => $catid])) { + throw new moodle_exception('nocategory', 'question'); +} + +$categorycontext = context::instance_by_id($category->contextid); +$category->context = $categorycontext; +// This page can be called without courseid or cmid in which case. +// We get the context from the category object. +if ($contexts === null) { // Need to get the course from the chosen category. + $contexts = new question_edit_contexts($categorycontext); + $thiscontext = $contexts->lowest(); + if ($thiscontext->contextlevel == CONTEXT_COURSE) { + require_login($thiscontext->instanceid, false); + } else if ($thiscontext->contextlevel == CONTEXT_MODULE) { + list($module, $cm) = get_module_from_cmid($thiscontext->instanceid); + require_login($cm->course, false, $cm); + } + $contexts->require_one_edit_tab_cap($edittab); +} + +$PAGE->set_url($thispageurl); + +$importform = new question_import_form($thispageurl, ['contexts' => $contexts->having_one_edit_tab_cap('import'), + 'defaultcategory' => $pagevars['cat']]); + +if ($importform->is_cancelled()) { + redirect($thispageurl); +} +// Page header. +$PAGE->set_title($txt->importquestions); +$PAGE->set_heading($COURSE->fullname); +echo $OUTPUT->header(); + +// Print horizontal nav if needed. +$renderer = $PAGE->get_renderer('core_question', 'bank'); +echo $renderer->extra_horizontal_navigation(); + +// File upload form submitted. +if ($form = $importform->get_data()) { + + // File checks out ok. + $fileisgood = false; + + // Work out if this is an uploaded file. + // Or one from the filesarea. + $realfilename = $importform->get_new_filename('newfile'); + $importfile = make_request_directory() . "/{$realfilename}"; + if (!$result = $importform->save_file('newfile', $importfile, true)) { + throw new moodle_exception('uploadproblem'); + } + + $formatfile = $CFG->dirroot . '/question/format/' . $form->format . '/format.php'; + if (!is_readable($formatfile)) { + throw new moodle_exception('formatnotfound', 'question', '', $form->format); + } + + require_once($formatfile); + + $classname = 'qformat_' . $form->format; + $qformat = new $classname(); + + // Load data into class. + $qformat->setCategory($category); + $qformat->setContexts($contexts->having_one_edit_tab_cap('import')); + $qformat->setCourse($COURSE); + $qformat->setFilename($importfile); + $qformat->setRealfilename($realfilename); + $qformat->setMatchgrades($form->matchgrades); + $qformat->setCatfromfile(!empty($form->catfromfile)); + $qformat->setContextfromfile(!empty($form->contextfromfile)); + $qformat->setStoponerror($form->stoponerror); + + // Do anything before that we need to. + if (!$qformat->importpreprocess()) { + throw new moodle_exception('cannotimport', '', $thispageurl->out()); + } + + // Process the uploaded file. + if (!$qformat->importprocess()) { + throw new moodle_exception('cannotimport', '', $thispageurl->out()); + } + + // In case anything needs to be done after. + if (!$qformat->importpostprocess()) { + throw new moodle_exception('cannotimport', '', $thispageurl->out()); + } + + // Log the import into this category. + $eventparams = [ + 'contextid' => $qformat->category->contextid, + 'other' => ['format' => $form->format, 'categoryid' => $qformat->category->id], + ]; + $event = \core\event\questions_imported::create($eventparams); + $event->trigger(); + + $params = $thispageurl->params() + ['category' => $qformat->category->id . ',' . $qformat->category->contextid]; + echo $OUTPUT->continue_button(new moodle_url('/question/edit.php', $params)); + echo $OUTPUT->footer(); + exit; +} + +echo $OUTPUT->heading_with_help($txt->importquestions, 'importquestions', 'question'); + +// Print upload form. +$importform->display(); +echo $OUTPUT->footer(); diff --git a/question/bank/importquestions/lang/en/qbank_importquestions.php b/question/bank/importquestions/lang/en/qbank_importquestions.php new file mode 100644 index 00000000000..0fb0cfe8e08 --- /dev/null +++ b/question/bank/importquestions/lang/en/qbank_importquestions.php @@ -0,0 +1,27 @@ +. + +/** + * Strings for component 'qbank_importquestions', language 'en' + * + * @package qbank_importquestions + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Marc-Alexandre Ghaly + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Import question bank feature'; +$string['privacy:metadata'] = 'The import question bank will import questions from a file according to the selected file format.'; diff --git a/question/bank/importquestions/version.php b/question/bank/importquestions/version.php new file mode 100644 index 00000000000..660d8bdbbf7 --- /dev/null +++ b/question/bank/importquestions/version.php @@ -0,0 +1,31 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @package qbank_importquestions + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Marc-Alexandre Ghaly + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'qbank_importquestions'; +$plugin->version = 2021070700; +$plugin->requires = 2021052500; +$plugin->maturity = MATURITY_STABLE;